blob: 180ff2773ed823dbe41c93d3848f4eef2177aca5 [file] [log] [blame]
library typed_mock;
_InvocationMatcher _lastMatcher;
/// Enables stubbing methods.
///
/// Use it when you want the mock to return a particular value when a particular
/// method, getter or setter is called.
///
/// when(obj.testProperty).thenReturn(10);
/// expect(obj.testProperty, 10); // pass
///
/// You can specify multiple matchers, which are checked one after another.
///
/// when(obj.testMethod(anyInt)).thenReturn('was int');
/// when(obj.testMethod(anyString)).thenReturn('was String');
/// expect(obj.testMethod(42), 'was int'); // pass
/// expect(obj.testMethod('foo'), 'was String'); // pass
///
/// You can even provide a function to calculate results.
/// Function can be also used to capture invocation arguments (if you test some
/// consumer).
///
/// when(obj.testMethod(anyInt)).thenInvoke((int p) => 10 + p);
/// expect(obj.testMethod(1), 11); // pass
/// expect(obj.testMethod(5), 15); // pass
Behavior when(_ignored) {
try {
var mock = _lastMatcher._mock;
mock._removeLastInvocation();
// set behavior
var behavior = new Behavior._(_lastMatcher);
_lastMatcher._behavior = behavior;
return behavior;
} finally {
// clear to prevent memory leak
_lastMatcher = null;
}
}
/// Clears all interactions remembered so far.
resetInteractions(TypedMock mock) {
mock._invocations.clear();
mock._verifiedInvocations.clear();
}
/// Verifies certain behavior happened a specified number of times.
Verifier verify(_ignored) {
try {
var mock = _lastMatcher._mock;
mock._removeLastInvocation();
// set verifier
return new Verifier._(mock, _lastMatcher);
} finally {
// clear to prevent memory leak
_lastMatcher = null;
}
}
/// Verifies that the given mock doesn't have any unverified interaction.
void verifyNoMoreInteractions(TypedMock mock) {
var notVerified = mock._computeNotVerifiedInvocations();
// OK
if (notVerified.isEmpty) {
return;
}
// fail
var invocationsString = _getInvocationsString(notVerified);
throw new VerifyError('Unexpected interactions:\n$invocationsString');
}
/// Verifies that no interactions happened on the given mock.
void verifyZeroInteractions(TypedMock mock) {
var invocations = mock._invocations;
// OK
if (invocations.isEmpty) {
return;
}
// fail
var invocationsString = _getInvocationsString(invocations);
throw new VerifyError('Unexpected interactions:\n$invocationsString');
}
/// [VerifyError] is thrown when one of the [verify] checks fails.
class VerifyError {
final String message;
VerifyError(this.message);
String toString() => 'VerifyError: $message';
}
String _getInvocationsString(Iterable<Invocation> invocations) {
var buffer = new StringBuffer();
invocations.forEach((invocation) {
var member = invocation.memberName;
buffer.write(member);
buffer.write(' ');
buffer.write(invocation.positionalArguments);
buffer.write(' ');
buffer.write(invocation.namedArguments);
buffer.writeln();
});
return buffer.toString();
}
class _InvocationMatcher {
final Symbol _member;
final TypedMock _mock;
final List<ArgumentMatcher> _matchers = [];
Behavior _behavior;
_InvocationMatcher(this._mock, this._member, Invocation invocation) {
invocation.positionalArguments.forEach((argument) {
ArgumentMatcher matcher;
if (argument is ArgumentMatcher) {
matcher = argument;
} else {
matcher = new _ArgumentMatcher_equals(argument);
}
_matchers.add(matcher);
});
}
bool match(Invocation invocation) {
var arguments = invocation.positionalArguments;
if (arguments.length != _matchers.length) {
return false;
}
for (int i = 0; i < _matchers.length; i++) {
var matcher = _matchers[i];
var argument = arguments[i];
if (!matcher.matches(argument)) {
return false;
}
}
return true;
}
}
class Behavior {
final _InvocationMatcher _matcher;
Behavior._(this._matcher);
bool _thenFunctionEnabled = false;
Function _thenFunction;
bool _returnAlwaysEnabled = false;
var _returnAlways;
bool _returnListEnabled = false;
List _returnList;
int _returnListIndex;
bool _throwExceptionEnabled = false;
var _throwException;
/// Invokes the given [function] with actual arguments and returns its result.
Behavior thenInvoke(Function function) {
_reset();
_thenFunctionEnabled = true;
_thenFunction = function;
return this;
}
/// Returns the specific value.
Behavior thenReturn(value) {
_reset();
_returnAlwaysEnabled = true;
_returnAlways = value;
return this;
}
/// Returns values from the [list] starting from first to the last.
/// If the end of list is reached a [StateError] is thrown.
Behavior thenReturnList(List list) {
_reset();
_returnListEnabled = true;
_returnList = list;
_returnListIndex = 0;
return this;
}
/// Throws the specified [exception] object.
Behavior thenThrow(exception) {
_reset();
_throwExceptionEnabled = true;
_throwException = exception;
return this;
}
_reset() {
_thenFunctionEnabled = false;
_returnAlwaysEnabled = false;
_returnListEnabled = false;
_throwExceptionEnabled = false;
}
dynamic _getReturnValue(Invocation invocation) {
// function
if (_thenFunctionEnabled) {
return Function.apply(_thenFunction, invocation.positionalArguments,
invocation.namedArguments);
}
// always
if (_returnAlwaysEnabled) {
return _returnAlways;
}
// list
if (_returnListEnabled) {
if (_returnListIndex >= _returnList.length) {
throw new StateError('All ${_returnList.length} elements for '
'${_matcher._member} from $_returnList have been exhausted.');
}
return _returnList[_returnListIndex++];
}
// exception
if (_throwExceptionEnabled) {
throw _throwException;
}
// no value
return null;
}
}
class Verifier {
final TypedMock _mock;
final _InvocationMatcher _matcher;
Verifier._(this._mock, this._matcher);
/// Marks matching interactions as verified and never fails.
void any() {
// mark as verified, but don't check the actual count
_count();
}
/// Verifies that there was no matching interactions.
void never() {
times(0);
}
/// Verifies that there was excatly one martching interaction.
void once() {
times(1);
}
/// Verifies that there was the specified number of matching interactions.
void times(int expected) {
var times = _count();
if (times != expected) {
var member = _matcher._member;
throw new VerifyError('$expected expected, but $times'
' invocations of $member recorded.');
}
}
/// Verifies that there was at least the specified number of matching
/// interactions.
void atLeast(int expected) {
var times = _count();
if (times < expected) {
var member = _matcher._member;
throw new VerifyError('At least $expected expected, but only $times'
' invocations of $member recorded.');
}
}
/// Verifies that there was at least one matching interaction.
void atLeastOnce() {
var times = _count();
if (times == 0) {
var member = _matcher._member;
throw new VerifyError('At least one expected, but only zero'
' invocations of $member recorded.');
}
}
/// Verifies that there was at most the specified number of matching
/// interactions.
void atMost(int expected) {
var times = _count();
if (times > expected) {
var member = _matcher._member;
throw new VerifyError('At most $expected expected, but $times'
' invocations of $member recorded.');
}
}
int _count() {
var times = 0;
_mock._invocations.forEach((invocation) {
if (invocation.memberName != _matcher._member) {
return;
}
if (!_matcher.match(invocation)) {
return;
}
_mock._verifiedInvocations.add(invocation);
times++;
});
return times;
}
}
/// A class to extend mocks from.
/// It supports specifying behavior using [when] and validation of interactions
/// using [verify].
///
/// abstract class Name {
/// String get firstName;
/// String get lastName;
/// }
/// class NameMock extends TypedMock implements Name {
/// noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
/// }
class TypedMock {
final Map<Symbol, List<_InvocationMatcher>> _matchersMap = {};
final List<Invocation> _invocations = [];
final Set<Invocation> _verifiedInvocations = new Set<Invocation>();
noSuchMethod(Invocation invocation) {
_invocations.add(invocation);
var member = invocation.memberName;
// prepare invocation matchers
var matchers = _matchersMap[member];
if (matchers == null) {
matchers = [];
_matchersMap[member] = matchers;
}
// check if there is a matcher
for (var matcher in matchers) {
if (matcher.match(invocation)) {
_lastMatcher = matcher;
// generate value if there is a behavior
if (matcher._behavior != null) {
return matcher._behavior._getReturnValue(invocation);
}
// probably verification
return null;
}
}
// add a new matcher
var matcher = new _InvocationMatcher(this, member, invocation);
matchers.add(matcher);
_lastMatcher = matcher;
}
Iterable<Invocation> _computeNotVerifiedInvocations() {
notVerified(e) => !_verifiedInvocations.contains(e);
return _invocations.where(notVerified);
}
void _removeLastInvocation() {
_invocations.removeLast();
}
}
/// [ArgumentMatcher] checks whether the given argument satisfies some
/// condition.
abstract class ArgumentMatcher {
const ArgumentMatcher();
/// Checks whether this matcher accepts the given argument.
bool matches(val);
}
class _ArgumentMatcher_equals extends ArgumentMatcher {
final expected;
const _ArgumentMatcher_equals(this.expected);
@override
bool matches(val) {
return val == expected;
}
}
class _ArgumentMatcher_anyBool extends ArgumentMatcher {
const _ArgumentMatcher_anyBool();
@override
bool matches(val) {
return val is bool;
}
}
/// Matches any [bool] value.
final anyBool = const _ArgumentMatcher_anyBool() as dynamic;
class _ArgumentMatcher_anyInt extends ArgumentMatcher {
const _ArgumentMatcher_anyInt();
@override
bool matches(val) {
return val is int;
}
}
/// Matches any [int] value.
final anyInt = const _ArgumentMatcher_anyInt() as dynamic;
class _ArgumentMatcher_anyObject extends ArgumentMatcher {
const _ArgumentMatcher_anyObject();
@override
bool matches(val) {
return true;
}
}
/// Matches any [Object] (or subclass) value.
final anyObject = const _ArgumentMatcher_anyObject() as dynamic;
class _ArgumentMatcher_anyString extends ArgumentMatcher {
const _ArgumentMatcher_anyString();
@override
bool matches(val) {
return val is String;
}
}
/// Matches any [String] value.
final anyString = const _ArgumentMatcher_anyString() as dynamic;