Merge branch 'master' into cleanup
diff --git a/lib/src/invocation_matcher.dart b/lib/src/invocation_matcher.dart
new file mode 100644
index 0000000..c176a1b
--- /dev/null
+++ b/lib/src/invocation_matcher.dart
@@ -0,0 +1,167 @@
+import 'package:collection/collection.dart';
+import 'package:matcher/matcher.dart';
+import 'package:meta/meta.dart';
+
+/// Returns a matcher that expects an invocation that matches arguments given.
+///
+/// Both [positionalArguments] and [namedArguments] can also be [Matcher]s:
+/// // Expects an invocation of "foo(String a, bool b)" where "a" must be
+/// // the value 'hello' but "b" may be any value. This would match both
+/// // foo('hello', true), foo('hello', false), and foo('hello', null).
+/// expect(fooInvocation, invokes(
+/// #foo,
+/// positionalArguments: ['hello', any]
+/// ));
+///
+/// Suitable for use in mocking libraries, where `noSuchMethod` can be used to
+/// get a handle to attempted [Invocation] objects and then compared against
+/// what a user expects to be called.
+Matcher invokes(
+ Symbol memberName, {
+ List positionalArguments: const [],
+ Map<Symbol, dynamic> namedArguments: const {},
+ bool isGetter: false,
+ bool isSetter: false,
+}) {
+ if (isGetter && isSetter) {
+ throw new ArgumentError('Cannot set isGetter and iSetter');
+ }
+ if (positionalArguments == null) {
+ throw new ArgumentError.notNull('positionalArguments');
+ }
+ if (namedArguments == null) {
+ throw new ArgumentError.notNull('namedArguments');
+ }
+ return new _InvocationMatcher(new _InvocationSignature(
+ memberName: memberName,
+ positionalArguments: positionalArguments,
+ namedArguments: namedArguments,
+ isGetter: isGetter,
+ isSetter: isSetter,
+ ));
+}
+
+/// Returns a matcher that matches the name and arguments of an [invocation].
+///
+/// To expect the same _signature_ see [invokes].
+Matcher isInvocation(Invocation invocation) =>
+ new _InvocationMatcher(invocation);
+
+class _InvocationSignature extends Invocation {
+ @override
+ final Symbol memberName;
+
+ @override
+ final List positionalArguments;
+
+ @override
+ final Map<Symbol, dynamic> namedArguments;
+
+ @override
+ final bool isGetter;
+
+ @override
+ final bool isSetter;
+
+ _InvocationSignature({
+ @required this.memberName,
+ this.positionalArguments: const [],
+ this.namedArguments: const {},
+ this.isGetter: false,
+ this.isSetter: false,
+ });
+
+ @override
+ bool get isMethod => !isAccessor;
+}
+
+class _InvocationMatcher implements Matcher {
+ static Description _describeInvocation(Description d, Invocation invocation) {
+ // For a getter or a setter, just return get <member> or set <member> <arg>.
+ if (invocation.isAccessor) {
+ d = d
+ .add(invocation.isGetter ? 'get ' : 'set ')
+ .add(_symbolToString(invocation.memberName));
+ if (invocation.isSetter) {
+ d = d.add(' ').addDescriptionOf(invocation.positionalArguments.first);
+ }
+ return d;
+ }
+ // For a method, return <member>(<args>).
+ d = d
+ .add(_symbolToString(invocation.memberName))
+ .add('(')
+ .addAll('', ', ', '', invocation.positionalArguments);
+ if (invocation.positionalArguments.isNotEmpty &&
+ invocation.namedArguments.isNotEmpty) {
+ d = d.add(', ');
+ }
+ // Also added named arguments, if any.
+ return d.addAll('', ', ', '', _namedArgsAndValues(invocation)).add(')');
+ }
+
+ // Returns named arguments as an iterable of '<name>: <value>'.
+ static Iterable<String> _namedArgsAndValues(Invocation invocation) =>
+ invocation.namedArguments.keys.map/*<String>*/((name) =>
+ '${_symbolToString(name)}: ${invocation.namedArguments[name]}');
+
+ // This will give is a mangled symbol in dart2js/aot with minification
+ // enabled, but it's safe to assume very few people will use the invocation
+ // matcher in a production test anyway due to noSuchMethod.
+ static String _symbolToString(Symbol symbol) {
+ return symbol.toString().split('"')[1];
+ }
+
+ final Invocation _invocation;
+
+ _InvocationMatcher(this._invocation) {
+ if (_invocation == null) {
+ throw new ArgumentError.notNull();
+ }
+ }
+
+ @override
+ Description describe(Description d) => _describeInvocation(d, _invocation);
+
+ // TODO(matanl): Better implement describeMismatch and use state from matches.
+ // Specifically, if a Matcher is passed as an argument, we'd like to get an
+ // error like "Expected fly(miles: > 10), Actual: fly(miles: 5)".
+ @override
+ Description describeMismatch(item, Description d, _, __) {
+ if (item is Invocation) {
+ d = d.add('Does not match ');
+ return _describeInvocation(d, item);
+ }
+ return d.add('Is not an Invocation');
+ }
+
+ @override
+ bool matches(item, _) =>
+ item is Invocation &&
+ _invocation.memberName == item.memberName &&
+ _invocation.isSetter == item.isSetter &&
+ _invocation.isGetter == item.isGetter &&
+ const ListEquality(const _MatcherEquality())
+ .equals(_invocation.positionalArguments, item.positionalArguments) &&
+ const MapEquality(values: const _MatcherEquality())
+ .equals(_invocation.namedArguments, item.namedArguments);
+}
+
+class _MatcherEquality extends DefaultEquality /* <Matcher | E> */ {
+ const _MatcherEquality();
+
+ @override
+ bool equals(e1, e2) {
+ if (e1 is Matcher && e2 is! Matcher) {
+ return e1.matches(e2, const {});
+ }
+ if (e2 is Matcher && e1 is! Matcher) {
+ return e2.matches(e1, const {});
+ }
+ return super.equals(e1, e2);
+ }
+
+ // We force collisions on every value so equals() is called.
+ @override
+ int hash(_) => 0;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 07298d5..cd508e3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: mockito
-version: 1.0.1+1
+version: 1.0.1+2
authors:
- Dmitriy Fibulwinter <fibulwinter@gmail.com>
- Ted Sander <tsander@google.com>
@@ -10,5 +10,6 @@
environment:
sdk: '>=1.0.0 <2.0.0'
dependencies:
- meta: '>=1.0.4 <2.0.0'
+ matcher: '^0.12.0'
+ meta: '^1.0.4'
test: '>=0.12.0 <0.13.0'
diff --git a/test/invocation_matcher_test.dart b/test/invocation_matcher_test.dart
new file mode 100644
index 0000000..d148196
--- /dev/null
+++ b/test/invocation_matcher_test.dart
@@ -0,0 +1,158 @@
+import 'package:mockito/src/invocation_matcher.dart';
+import 'package:test/test.dart';
+
+Invocation lastInvocation;
+
+void main() {
+ const stub = const Stub();
+
+ group('$isInvocation', () {
+ test('positional arguments', () {
+ var call1 = stub.say('Hello');
+ var call2 = stub.say('Hello');
+ var call3 = stub.say('Guten Tag');
+ shouldPass(call1, isInvocation(call2));
+ shouldFail(
+ call1,
+ isInvocation(call3),
+ "Expected: say('Guten Tag') "
+ "Actual: <Instance of '${call3.runtimeType}'> "
+ "Which: Does not match say('Hello')",
+ );
+ });
+
+ test('named arguments', () {
+ var call1 = stub.eat('Chicken', alsoDrink: true);
+ var call2 = stub.eat('Chicken', alsoDrink: true);
+ var call3 = stub.eat('Chicken', alsoDrink: false);
+ shouldPass(call1, isInvocation(call2));
+ shouldFail(
+ call1,
+ isInvocation(call3),
+ "Expected: eat('Chicken', 'alsoDrink: false') "
+ "Actual: <Instance of '${call3.runtimeType}'> "
+ "Which: Does not match eat('Chicken', 'alsoDrink: true')",
+ );
+ });
+
+ test('optional arguments', () {
+ var call1 = stub.lie(true);
+ var call2 = stub.lie(true);
+ var call3 = stub.lie(false);
+ shouldPass(call1, isInvocation(call2));
+ shouldFail(
+ call1,
+ isInvocation(call3),
+ "Expected: lie(<false>) "
+ "Actual: <Instance of '${call3.runtimeType}'> "
+ "Which: Does not match lie(<true>)",
+ );
+ });
+
+ test('getter', () {
+ var call1 = stub.value;
+ var call2 = stub.value;
+ stub.value = true;
+ var call3 = Stub.lastInvocation;
+ shouldPass(call1, isInvocation(call2));
+ shouldFail(
+ call1,
+ isInvocation(call3),
+ "Expected: set value= <true> "
+ "Actual: <Instance of '${call3.runtimeType}'> "
+ "Which: Does not match get value",
+ );
+ });
+
+ test('setter', () {
+ stub.value = true;
+ var call1 = Stub.lastInvocation;
+ stub.value = true;
+ var call2 = Stub.lastInvocation;
+ stub.value = false;
+ var call3 = Stub.lastInvocation;
+ shouldPass(call1, isInvocation(call2));
+ shouldFail(
+ call1,
+ isInvocation(call3),
+ "Expected: set value= <false> "
+ "Actual: <Instance of '${call3.runtimeType}'> "
+ "Which: Does not match set value= <true>",
+ );
+ });
+ });
+
+ group('$invokes', () {
+ test('positional arguments', () {
+ var call = stub.say('Hello');
+ shouldPass(call, invokes(#say, positionalArguments: ['Hello']));
+ shouldPass(call, invokes(#say, positionalArguments: [anything]));
+ shouldFail(
+ call,
+ invokes(#say, positionalArguments: [isNull]),
+ "Expected: say(null) "
+ "Actual: <Instance of '${call.runtimeType}'> "
+ "Which: Does not match say('Hello')",
+ );
+ });
+
+ test('named arguments', () {
+ var call = stub.fly(miles: 10);
+ shouldPass(call, invokes(#fly, namedArguments: {#miles: 10}));
+ shouldPass(call, invokes(#fly, namedArguments: {#miles: greaterThan(5)}));
+ shouldFail(
+ call,
+ invokes(#fly, namedArguments: {#miles: 11}),
+ "Expected: fly('miles: 11') "
+ "Actual: <Instance of '${call.runtimeType}'> "
+ "Which: Does not match fly('miles: 10')",
+ );
+ });
+ });
+}
+
+abstract class Interface {
+ bool get value;
+ set value(value);
+ say(String text);
+ eat(String food, {bool alsoDrink});
+ lie([bool facingDown]);
+ fly({int miles});
+}
+
+/// An example of a class that captures Invocation objects.
+///
+/// Any call always returns an [Invocation].
+class Stub implements Interface {
+ static Invocation lastInvocation;
+
+ const Stub();
+
+ @override
+ noSuchMethod(Invocation invocation) => lastInvocation = invocation;
+}
+
+// Copied from package:test, which doesn't expose it to users.
+// https://github.com/dart-lang/matcher/issues/39
+void shouldFail(value, Matcher matcher, expected) {
+ var failed = false;
+ try {
+ expect(value, matcher);
+ } on TestFailure catch (err) {
+ failed = true;
+
+ var _errorString = err.message;
+
+ if (expected is String) {
+ expect(_errorString, equalsIgnoringWhitespace(expected));
+ } else {
+ expect(_errorString.replaceAll('\n', ''), expected);
+ }
+ }
+
+ expect(failed, isTrue, reason: 'Expected to fail.');
+}
+
+void shouldPass(value, Matcher matcher) {
+ expect(value, matcher);
+}
diff --git a/test/mockito_test.dart b/test/mockito_test.dart
index 2e07913..2dd4b9b 100644
--- a/test/mockito_test.dart
+++ b/test/mockito_test.dart
@@ -13,11 +13,13 @@
String methodWithObjArgs(RealClass x) => "Real";
// "SpecialArgs" here means type-parameterized args. But that makes for a long
// method name.
- String typeParameterizedFn(
- List<int> w, List<int> x, [List<int> y, List<int> z]) => "Real";
+ String typeParameterizedFn(List<int> w, List<int> x,
+ [List<int> y, List<int> z]) =>
+ "Real";
// "SpecialNamedArgs" here means type-parameterized, named args. But that
// makes for a long method name.
- String typeParameterizedNamedFn(List<int> w, List<int> x, {List<int> y, List<int> z}) =>
+ String typeParameterizedNamedFn(List<int> w, List<int> x,
+ {List<int> y, List<int> z}) =>
"Real";
String get getter => "Real";
void set setter(String arg) {
@@ -85,7 +87,6 @@
});
});
-
group("mixin support", () {
test("should work", () {
var foo = new MockFoo();
@@ -143,16 +144,16 @@
expect(mock.methodWithListArgs([42]), equals("A lot!"));
expect(mock.methodWithListArgs([43]), equals("A lot!"));
});
- test("should mock method with multiple named args and matchers", (){
+ test("should mock method with multiple named args and matchers", () {
when(mock.methodWithTwoNamedArgs(any, y: any)).thenReturn("x y");
when(mock.methodWithTwoNamedArgs(any, z: any)).thenReturn("x z");
expect(mock.methodWithTwoNamedArgs(42), isNull);
- expect(mock.methodWithTwoNamedArgs(42, y:18), equals("x y"));
- expect(mock.methodWithTwoNamedArgs(42, z:17), equals("x z"));
- expect(mock.methodWithTwoNamedArgs(42, y:18, z:17), isNull);
+ expect(mock.methodWithTwoNamedArgs(42, y: 18), equals("x y"));
+ expect(mock.methodWithTwoNamedArgs(42, z: 17), equals("x z"));
+ expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), isNull);
when(mock.methodWithTwoNamedArgs(any, y: any, z: any))
.thenReturn("x y z");
- expect(mock.methodWithTwoNamedArgs(42, y:18, z:17), equals("x y z"));
+ expect(mock.methodWithTwoNamedArgs(42, y: 18, z: 17), equals("x y z"));
});
test("should mock method with mix of argument matchers and real things",
() {
@@ -231,17 +232,19 @@
.thenReturn("A lot!");
expect(mock.typeParameterizedFn([42], [43], [44]), equals("A lot!"));
});
- test("should mock method with an optional typed arg matcher and an optional real arg", () {
+ test(
+ "should mock method with an optional typed arg matcher and an optional real arg",
+ () {
when(mock.typeParameterizedFn(typed(any), typed(any), [44], typed(any)))
.thenReturn("A lot!");
- expect(mock.typeParameterizedFn([42], [43], [44], [45]), equals("A lot!"));
+ expect(
+ mock.typeParameterizedFn([42], [43], [44], [45]), equals("A lot!"));
});
test("should mock method with only some typed arg matchers", () {
when(mock.typeParameterizedFn(typed(any), [43], typed(any)))
.thenReturn("A lot!");
expect(mock.typeParameterizedFn([42], [43], [44]), equals("A lot!"));
- when(mock.typeParameterizedFn(typed(any), [43]))
- .thenReturn("A bunch!");
+ when(mock.typeParameterizedFn(typed(any), [43])).thenReturn("A bunch!");
expect(mock.typeParameterizedFn([42], [43]), equals("A bunch!"));
});
test("should throw when [typed] used alongside [null].", () {
@@ -252,47 +255,51 @@
});
test("should mock method when [typed] used alongside matched [null].", () {
when(mock.typeParameterizedFn(
- typed(any), argThat(equals(null)), typed(any)))
+ typed(any), argThat(equals(null)), typed(any)))
.thenReturn("A lot!");
expect(mock.typeParameterizedFn([42], null, [44]), equals("A lot!"));
});
test("should mock method with named, typed arg matcher", () {
- when(mock.typeParameterizedNamedFn(
- typed(any), [43], y: typed(any, named: "y")))
+ when(mock.typeParameterizedNamedFn(typed(any), [43],
+ y: typed(any, named: "y")))
.thenReturn("A lot!");
- expect(mock.typeParameterizedNamedFn([42], [43], y: [44]), equals("A lot!"));
+ expect(
+ mock.typeParameterizedNamedFn([42], [43], y: [44]), equals("A lot!"));
});
- test("should mock method with named, typed arg matcher and an arg matcher", () {
- when(
- mock.typeParameterizedNamedFn(
- typed(any), [43],
+ test("should mock method with named, typed arg matcher and an arg matcher",
+ () {
+ when(mock.typeParameterizedNamedFn(typed(any), [43],
y: typed(any, named: "y"), z: argThat(contains(45))))
.thenReturn("A lot!");
expect(mock.typeParameterizedNamedFn([42], [43], y: [44], z: [45]),
equals("A lot!"));
});
- test("should mock method with named, typed arg matcher and a regular arg", () {
- when(
- mock.typeParameterizedNamedFn(
- typed(any), [43],
- y: typed(any, named: "y"), z: [45]))
- .thenReturn("A lot!");
+ test("should mock method with named, typed arg matcher and a regular arg",
+ () {
+ when(mock.typeParameterizedNamedFn(typed(any), [43],
+ y: typed(any, named: "y"), z: [45])).thenReturn("A lot!");
expect(mock.typeParameterizedNamedFn([42], [43], y: [44], z: [45]),
equals("A lot!"));
});
test("should throw when [typed] used as a named arg, without `named:`", () {
- expect(() => when(mock.typeParameterizedNamedFn(
- typed(any), [43], y: typed(any))),
+ expect(
+ () => when(
+ mock.typeParameterizedNamedFn(typed(any), [43], y: typed(any))),
throwsArgumentError);
});
- test("should throw when [typed] used as a positional arg, with `named:`", () {
- expect(() => when(mock.typeParameterizedNamedFn(
- typed(any), typed(any, named: "y"))),
+ test("should throw when [typed] used as a positional arg, with `named:`",
+ () {
+ expect(
+ () => when(mock.typeParameterizedNamedFn(
+ typed(any), typed(any, named: "y"))),
throwsArgumentError);
});
- test("should throw when [typed] used as a named arg, with the wrong `named:`", () {
- expect(() => when(mock.typeParameterizedNamedFn(
- typed(any), [43], y: typed(any, named: "z"))),
+ test(
+ "should throw when [typed] used as a named arg, with the wrong `named:`",
+ () {
+ expect(
+ () => when(mock.typeParameterizedNamedFn(typed(any), [43],
+ y: typed(any, named: "z"))),
throwsArgumentError);
});
});
@@ -344,9 +351,9 @@
mock.methodWithObjArgs(m1);
expectFail(
"No matching calls. All calls: MockedClass.methodWithObjArgs(m1)",
- () {
- verify(mock.methodWithObjArgs(new MockedClass()));
- });
+ () {
+ verify(mock.methodWithObjArgs(new MockedClass()));
+ });
verify(mock.methodWithObjArgs(m1));
});
test("should mock method with list args", () {
@@ -376,10 +383,15 @@
test("should mock method with argument matcher and capturer", () {
mock.methodWithNormalArgs(50);
mock.methodWithNormalArgs(100);
- expect(verify(mock.methodWithNormalArgs(
- captureThat(greaterThan(75)))).captured.single, equals(100));
- expect(verify(mock
- .methodWithNormalArgs(captureThat(lessThan(75)))).captured.single,
+ expect(
+ verify(mock.methodWithNormalArgs(captureThat(greaterThan(75))))
+ .captured
+ .single,
+ equals(100));
+ expect(
+ verify(mock.methodWithNormalArgs(captureThat(lessThan(75))))
+ .captured
+ .single,
equals(50));
});
test("should mock method with mix of argument matchers and real things",
@@ -417,9 +429,12 @@
test("should verify method with argument capturer", () {
mock.typeParameterizedFn([50], [17]);
mock.typeParameterizedFn([100], [17]);
- expect(verify(mock.typeParameterizedFn(
- typed(captureAny), [17])).captured,
- equals([[50], [100]]));
+ expect(
+ verify(mock.typeParameterizedFn(typed(captureAny), [17])).captured,
+ equals([
+ [50],
+ [100]
+ ]));
});
});
group("verify() qualifies", () {
@@ -580,21 +595,22 @@
});
test("should captureOut list arguments", () {
mock.methodWithListArgs([42]);
- expect(verify(
- mock.methodWithListArgs(captureAny)).captured.single,
+ expect(verify(mock.methodWithListArgs(captureAny)).captured.single,
equals([42]));
});
test("should captureOut multiple arguments", () {
mock.methodWithPositionalArgs(1, 2);
- expect(verify(
- mock.methodWithPositionalArgs(captureAny, captureAny)).captured,
+ expect(
+ verify(mock.methodWithPositionalArgs(captureAny, captureAny))
+ .captured,
equals([1, 2]));
});
test("should captureOut with matching arguments", () {
mock.methodWithPositionalArgs(1);
mock.methodWithPositionalArgs(2, 3);
- expect(verify(
- mock.methodWithPositionalArgs(captureAny, captureAny)).captured,
+ expect(
+ verify(mock.methodWithPositionalArgs(captureAny, captureAny))
+ .captured,
equals([2, 3]));
});
test("should captureOut multiple invocations", () {
diff --git a/tool/travis.sh b/tool/travis.sh
index 5387de5..e22fa98 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -6,8 +6,10 @@
# Verify that the libraries are error free.
dartanalyzer --fatal-warnings \
lib/mockito.dart \
+ lib/mockito_no_mirrors.dart \
+ lib/src/invocation_matcher.dart \
test/mockito_test.dart
# Run the tests.
-dart test/mockito_test.dart
-
+dart -c test/invocation_matcher_test.dart
+dart -c test/mockito_test.dart