From e32f79a9093ddb9617c737c89a713f5fcd73b3bc Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 16 Jun 2016 08:03:05 -0700 Subject: [PATCH 1/2] Add strong mode-compliant 'typed' API --- README.md | 89 +++++++++++++++++++++++- lib/src/mock.dart | 151 +++++++++++++++++++++++++++++++++++++++++ test/mockito_test.dart | 120 +++++++++++++++++++++++++++++++- 3 files changed, 357 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 36e93160..883584e2 100644 --- a/README.md +++ b/README.md @@ -126,11 +126,11 @@ verifyNoMoreInteractions(cat); ```dart //simple capture cat.eatFood("Fish"); -expect(verify(cat.eatFood(capture)).captured.single, "Fish"); +expect(verify(cat.eatFood(captureAny)).captured.single, "Fish"); //capture multiple calls cat.eatFood("Milk"); cat.eatFood("Fish"); -expect(verify(cat.eatFood(capture)).captured, ["Milk", "Fish"]); +expect(verify(cat.eatFood(captureAny)).captured, ["Milk", "Fish"]); //conditional capture cat.eatFood("Milk"); cat.eatFood("Fish"); @@ -147,6 +147,91 @@ expect(cat.sound(), "Purr"); //using real object expect(cat.lives, 9); ``` + +## Strong mode compliance + +Unfortunately, the use of the arg matchers in mock method calls (like `cat.eatFood(any)`) +violates the [Strong mode] type system. Specifically, if the method signature of a mocked +method has a parameter with a parameterized type (like `List`), then passing `any` or +`argThat` will result in a Strong mode warning: + +> [warning] Unsound implicit cast from dynamic to List<int> + +In order to write Strong mode-compliant tests with Mockito, you might need to use `typed`, +annotating it with a type parameter comment. Let's use a slightly different `Cat` class to +show some examples: + +```dart +class Cat { + bool eatFood(List foods, [List mixins]) => true; + int walk(List places, {Map gaits}) => 0; +} + +class MockCat extends Mock implements Cat {} + +var cat = new MockCat(); +``` + +OK, what if we try to stub using `any`: + +```dart +when(cat.eatFood(any)).thenReturn(true); +``` + +Let's analyze this code: + +``` +$ dartanalyzer --strong test/cat_test.dart +Analyzing [lib/cat_test.dart]... +[warning] Unsound implicit cast from dynamic to List (test/cat_test.dart, line 12, col 20) +1 warning found. +``` + +This code is not Strong mode-compliant. Let's change it to use `typed`: + +```dart +when(cat.eatFood(typed/*>*/(any))) +``` + +``` +$ dartanalyzer --strong test/cat_test.dart +Analyzing [lib/cat_test.dart]... +No issues found +``` + +Great! A little ugly, but it works. Here are some more examples: + +```dart +when(cat.eatFood(typed/*>*/(any), typed/*>*/(any))) + .thenReturn(true); +when(cat.eatFood(typed/*>*/(argThat(contains("fish"))))) + .thenReturn(true); +``` + +Named args require one more component: `typed` needs to know what named argument it is +being passed into: + +```dart +when(cat.walk( + typed/*>*/(any), + gaits: typed/*>*/(any), name: 'gaits')).thenReturn(true); +``` + +Note the `name` argument. Mockito should fail gracefully if you forget to name a `typed` +call passed in as a named argument, or name the argument incorrectly. + +One more note about the `typed` API: you cannot mix `typed` arguments with `null` +arguments: + +```dart +when(cat.eatFood(null, typed/*>*/(any))).thenReturn(true) // Throws! +when(cat.eatFood( + argThat(equals(null)), + typed/*>*/(any))).thenReturn(true); // Works. +``` + +[Strong mode]: https://github.com/dart-lang/dev_compiler/blob/master/STRONG_MODE.md + ## How it works The basics of the `Mock` class are nothing special: It uses `noSuchMethod` to catch all method invocations, and returns the value that you have configured beforehand with diff --git a/lib/src/mock.dart b/lib/src/mock.dart index 71b8d9ce..d4bcfa9b 100644 --- a/lib/src/mock.dart +++ b/lib/src/mock.dart @@ -10,6 +10,8 @@ _WhenCall _whenCall = null; final List<_VerifyCall> _verifyCalls = <_VerifyCall>[]; final _TimeStampProvider _timer = new _TimeStampProvider(); final List _capturedArgs = []; +final List<_ArgMatcher> _typedArgs = <_ArgMatcher>[]; +final Map _typedNamedArgs = {}; // Hidden from the public API, used by spy.dart. void setDefaultResponse(Mock mock, dynamic defaultResponse) { @@ -31,6 +33,9 @@ class Mock { } dynamic noSuchMethod(Invocation invocation) { + if (_typedArgs.isNotEmpty || _typedNamedArgs.isNotEmpty) { + invocation = _reconstituteInvocation(invocation); + } if (_whenInProgress) { _whenCall = new _WhenCall(this, invocation); return null; @@ -55,6 +60,132 @@ class Mock { String toString() => _givenName != null ? _givenName : runtimeType.toString(); } +// Return a new [Invocation], reconstituted from [invocation], [_typedArgs], +// and [_typedNamedArgs]. +Invocation _reconstituteInvocation(Invocation invocation) { + var newInvocation = new FakeInvocation(invocation); + return newInvocation; +} + +/// An Invocation implementation that allows all attributes to be passed into +/// the constructor. +class FakeInvocation extends Invocation { + final Symbol memberName; + final Map namedArguments; + final List positionalArguments; + final bool isGetter; + final bool isMethod; + final bool isSetter; + + factory FakeInvocation(Invocation invocation) { + if (_typedArgs.isEmpty && _typedNamedArgs.isEmpty) { + throw new StateError("FakeInvocation called when no typed calls have been saved."); + } + + // Handle named arguments first, so that we can provide useful errors for + // the various bad states. If all is well with the named arguments, then we + // can process the positional arguments, and resort to more general errors + // if the state is still bad. + var namedArguments = _reconstituteNamedArgs(invocation); + var positionalArguments = _reconstitutePositionalArgs(invocation); + + _typedArgs.clear(); + _typedNamedArgs.clear(); + + return new FakeInvocation._( + invocation.memberName, + positionalArguments, + namedArguments, + invocation.isGetter, + invocation.isMethod, + invocation.isSetter); + } + + static Map _reconstituteNamedArgs(Invocation invocation) { + var namedArguments = {}; + var _typedNamedArgSymbols = _typedNamedArgs.keys.map((name) => new Symbol(name)); + invocation.namedArguments.forEach((name, arg) { + if (arg == null) { + if (!_typedNamedArgSymbols.contains(name)) { + // Incorrect usage of [typed], something like: + // `when(obj.fn(a: typed(any)))`. + throw new ArgumentError( + 'A typed argument was passed in as a named argument named "$name", ' + 'but did not a value for its name. Each typed argument that is ' + 'passed as a named argument needs to specify the `name` argument. ' + 'For example: `when(obj.fn(x: typed(any, name: "x")))`.'); + } + } else { + // Add each real named argument that was _not_ passed with [typed]. + namedArguments[name] = arg; + } + }); + + _typedNamedArgs.forEach((name, arg) { + Symbol nameSymbol = new Symbol(name); + if (!invocation.namedArguments.containsKey(nameSymbol)) { + // Incorrect usage of [name], something like: + // `when(obj.fn(typed(any, name: 'a')))`. + throw new ArgumentError( + 'A typed argument was declared with name $name, but was not passed ' + 'as an argument named $name.'); + } + if (invocation.namedArguments[nameSymbol] != null) { + // Incorrect usage of [name], something like: + // `when(obj.fn(a: typed(any, name: 'b'), b: "string"))`. + throw new ArgumentError( + 'A typed argument was declared with name $name, but a different ' + 'value (${invocation.namedArguments[nameSymbol]}) was passed as ' + '$name.'); + } + namedArguments[nameSymbol] = arg; + }); + + return namedArguments; + } + + static List _reconstitutePositionalArgs(Invocation invocation) { + var positionalArguments = []; + var nullPositionalArguments = + invocation.positionalArguments.where((arg) => arg == null); + if (_typedArgs.length != nullPositionalArguments.length) { + throw new ArgumentError( + 'null arguments are not allowed alongside typed(); use ' + '"typed(eq(null))"'); + } + int i = 0; + int j = 0; + while (i < _typedArgs.length && j < invocation.positionalArguments.length) { + var arg = _typedArgs[i]; + if (invocation.positionalArguments[j] == null) { + // [typed] was used; add the [_ArgMatcher] given to [typed]. + positionalArguments.add(arg); + i++; + j++; + } else { + // [typed] was not used; add the [_ArgMatcher] from [invocation]. + positionalArguments.add(invocation.positionalArguments[j]); + j++; + } + } + while (j < invocation.positionalArguments.length) { + // Some trailing non-[typed] arguments. + positionalArguments.add(invocation.positionalArguments[j]); + j++; + } + + return positionalArguments; + } + + FakeInvocation._( + this.memberName, + this.positionalArguments, + this.namedArguments, + this.isGetter, + this.isMethod, + this.isSetter); +} + named(var mock, {String name, int hashCode}) => mock .._givenName = name .._givenHashCode = hashCode; @@ -300,6 +431,15 @@ get captureAny => new _ArgMatcher(anything, true); captureThat(Matcher matcher) => new _ArgMatcher(matcher, true); argThat(Matcher matcher) => new _ArgMatcher(matcher, false); +/*=T*/ typed/**/(_ArgMatcher matcher, {String name}) { + if (name == null) { + _typedArgs.add(matcher); + } else { + _typedNamedArgs[name] = matcher; + } + return null; +} + class VerificationResult { List captured = []; int callCount; @@ -413,3 +553,14 @@ void logInvocations(List mocks) { print(inv.toString()); }); } + +/// Should only be used during Mockito testing. +void resetMockitoState() { + _whenInProgress = false; + _verificationInProgress = false; + _whenCall = null; + _verifyCalls.clear(); + _capturedArgs.clear(); + _typedArgs.clear(); + _typedNamedArgs.clear(); +} diff --git a/test/mockito_test.dart b/test/mockito_test.dart index 03a2ed95..461643f6 100644 --- a/test/mockito_test.dart +++ b/test/mockito_test.dart @@ -9,6 +9,14 @@ class RealClass { String methodWithNamedArgs(int x, {int y}) => "Real"; String methodWithTwoNamedArgs(int x, {int y, int z}) => "Real"; String methodWithObjArgs(RealClass x) => "Real"; + // "SpecialArgs" here means type-parameterized args. But that makes for a long + // method name. + String methodWithSpecialArgs( + List w, List x, [List y, List z]) => "Real"; + // "SpecialNamedArgs" here means type-parameterized, named args. But that + // makes for a long method name. + String methodWithSpecialNamedArgs(List w, List x, {List y, List z}) => + "Real"; String get getter => "Real"; void set setter(String arg) { throw new StateError("I must be mocked"); @@ -55,6 +63,12 @@ void main() { mock = new MockedClass(); }); + tearDown(() { + // In some of the tests that expect an Error to be thrown, Mockito's + // global state can become invalid. Reset it. + resetMockitoState(); + }); + group("spy", () { setUp(() { mock = spy(new MockedClass(), new RealClass()); @@ -204,6 +218,96 @@ void main() { when(mock.methodWithNormalArgs(argThat(equals(42)))).thenReturn("42"); expect(mock.methodWithNormalArgs(43), equals("43")); }); + test("should mock method with typed arg matchers", () { + when(mock.methodWithSpecialArgs( + typed/*>*/(any), typed/*>*/(any))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialArgs([42], [43]), equals("A lot!")); + expect(mock.methodWithSpecialArgs([43], [44]), equals("A lot!")); + }); + test("should mock method with an optional typed arg matcher", () { + when(mock.methodWithSpecialArgs( + typed/*>*/(any), + typed/*>*/(any), + typed/*>*/(any))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialArgs([42], [43], [44]), equals("A lot!")); + }); + test("should mock method with an optional typed arg matcher and an optional real arg", () { + when(mock.methodWithSpecialArgs( + typed/*>*/(any), + typed/*>*/(any), + [44], + typed/*>*/(any))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialArgs([42], [43], [44], [45]), equals("A lot!")); + }); + test("should mock method with only some typed arg matchers", () { + when(mock.methodWithSpecialArgs( + typed/*>*/(any), [43], typed/*>*/(any))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialArgs([42], [43], [44]), equals("A lot!")); + when(mock.methodWithSpecialArgs(typed/*>*/(any), [43])) + .thenReturn("A bunch!"); + expect(mock.methodWithSpecialArgs([42], [43]), equals("A bunch!")); + }); + test("should throw when [typed] used alongside [null].", () { + expect(() => when(mock.methodWithSpecialArgs( + typed/*>*/(any), null, typed/*>*/(any))), + throwsArgumentError); + expect(() => when(mock.methodWithSpecialArgs( + typed/*>*/(any), typed/*>*/(any), null)), + throwsArgumentError); + }); + test("should mock method when [typed] used alongside matched [null].", () { + when(mock.methodWithSpecialArgs( + typed/*>*/(any), argThat(equals(null)), typed/*>*/(any))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialArgs([42], null, [44]), equals("A lot!")); + }); + test("should mock method with named, typed arg matcher", () { + when(mock.methodWithSpecialNamedArgs( + typed/*>*/(any), [43], y: typed/*>*/(any, name: "y"))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialNamedArgs([42], [43], y: [44]), equals("A lot!")); + }); + test("should mock method with named, typed arg matcher and an arg matcher", () { + when( + mock.methodWithSpecialNamedArgs( + typed/*>*/(any), + [43], + y: typed/*>*/(any, name: "y"), + z: argThat(contains(45)))) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialNamedArgs([42], [43], y: [44], z: [45]), + equals("A lot!")); + }); + test("should mock method with named, typed arg matcher and a regular arg", () { + when( + mock.methodWithSpecialNamedArgs( + typed/*>*/(any), + [43], + y: typed/*>*/(any, name: "y"), + z: [45])) + .thenReturn("A lot!"); + expect(mock.methodWithSpecialNamedArgs([42], [43], y: [44], z: [45]), + equals("A lot!")); + }); + test("should throw when [typed] used as a named arg, without `name:`", () { + expect(() => when(mock.methodWithSpecialNamedArgs( + typed/*>*/(any), [43], y: typed/*>*/(any))), + throwsArgumentError); + }); + test("should throw when [typed] used as a positional arg, with `name:`", () { + expect(() => when(mock.methodWithSpecialNamedArgs( + typed/*>*/(any), typed/*>*/(any, name: "y"))), + throwsArgumentError); + }); + test("should throw when [typed] used as a named arg, with the wrong `name:`", () { + expect(() => when(mock.methodWithSpecialNamedArgs( + typed/*>*/(any), [43], y: typed/*>*/(any, name: "z"))), + throwsArgumentError); + }); }); group("verify()", () { @@ -319,6 +423,18 @@ void main() { }); verify(mock.setter = "A"); }); + test("should verify method with typed arg matchers", () { + mock.methodWithSpecialArgs([42], [43]); + verify(mock.methodWithSpecialArgs( + typed/*>*/(any), typed/*>*/(any))); + }); + test("should verify method with argument capturer", () { + mock.methodWithSpecialArgs([50], [17]); + mock.methodWithSpecialArgs([100], [17]); + expect(verify(mock.methodWithSpecialArgs( + typed/*>*/(captureAny), [17])).captured, + equals([[50], [100]])); + }); }); group("verify() qualifies", () { group("unqualified as at least one", () { @@ -478,7 +594,9 @@ void main() { }); test("should captureOut list arguments", () { mock.methodWithListArgs([42]); - expect(verify(mock.methodWithListArgs(captureAny)).captured.single, equals([42])); + expect(verify( + mock.methodWithListArgs(captureAny)).captured.single, + equals([42])); }); test("should captureOut multiple arguments", () { mock.methodWithPositionalArgs(1, 2); From beb8b26f4d49cc87aa8d3315d3ccf62924d56922 Mon Sep 17 00:00:00 2001 From: Sam Rawlins Date: Thu, 7 Jul 2016 08:54:04 -0700 Subject: [PATCH 2/2] Address feedback, remove type annotations --- CHANGELOG.md | 11 +++++ README.md | 19 ++++---- lib/src/mock.dart | 87 ++++++++++++++++++---------------- pubspec.yaml | 2 +- test/mockito_test.dart | 104 +++++++++++++++++------------------------ 5 files changed, 110 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c1f1506..9534f53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.0.0 + +* Add a new `typed` API that is compatible with Dart Dev Compiler; documented in + README.md. + +## 0.11.1 + +* Move the reflection-based `spy` code into a private source file. Now + `package:mockito/mockito.dart` includes this reflection-based API, and a new + `package:mockito/mockito_no_mirrors.dart` doesn't require mirrors. + ## 0.11.0 * Equality matcher used by default to simplify matching collections as arguments. Should be non-breaking change in most cases, otherwise consider using `argThat(identical(arg))`. diff --git a/README.md b/README.md index 883584e2..d5e362ce 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ Analyzing [lib/cat_test.dart]... This code is not Strong mode-compliant. Let's change it to use `typed`: ```dart -when(cat.eatFood(typed/*>*/(any))) +when(cat.eatFood(typed(any))) ``` ``` @@ -202,32 +202,29 @@ No issues found Great! A little ugly, but it works. Here are some more examples: ```dart -when(cat.eatFood(typed/*>*/(any), typed/*>*/(any))) - .thenReturn(true); -when(cat.eatFood(typed/*>*/(argThat(contains("fish"))))) - .thenReturn(true); +when(cat.eatFood(typed(any), typed(any))).thenReturn(true); +when(cat.eatFood(typed(argThat(contains("fish"))))).thenReturn(true); ``` Named args require one more component: `typed` needs to know what named argument it is being passed into: ```dart -when(cat.walk( - typed/*>*/(any), - gaits: typed/*>*/(any), name: 'gaits')).thenReturn(true); +when(cat.walk(typed(any), gaits: typed(any, named: 'gaits'))) + .thenReturn(true); ``` -Note the `name` argument. Mockito should fail gracefully if you forget to name a `typed` +Note the `named` argument. Mockito should fail gracefully if you forget to name a `typed` call passed in as a named argument, or name the argument incorrectly. One more note about the `typed` API: you cannot mix `typed` arguments with `null` arguments: ```dart -when(cat.eatFood(null, typed/*>*/(any))).thenReturn(true) // Throws! +when(cat.eatFood(null, typed(any))).thenReturn(true); // Throws! when(cat.eatFood( argThat(equals(null)), - typed/*>*/(any))).thenReturn(true); // Works. + typed(any))).thenReturn(true); // Works. ``` [Strong mode]: https://github.com/dart-lang/dev_compiler/blob/master/STRONG_MODE.md diff --git a/lib/src/mock.dart b/lib/src/mock.dart index d4bcfa9b..c1bf3860 100644 --- a/lib/src/mock.dart +++ b/lib/src/mock.dart @@ -34,7 +34,7 @@ class Mock { dynamic noSuchMethod(Invocation invocation) { if (_typedArgs.isNotEmpty || _typedNamedArgs.isNotEmpty) { - invocation = _reconstituteInvocation(invocation); + invocation = new _InvocationForTypedArguments(invocation); } if (_whenInProgress) { _whenCall = new _WhenCall(this, invocation); @@ -60,16 +60,9 @@ class Mock { String toString() => _givenName != null ? _givenName : runtimeType.toString(); } -// Return a new [Invocation], reconstituted from [invocation], [_typedArgs], -// and [_typedNamedArgs]. -Invocation _reconstituteInvocation(Invocation invocation) { - var newInvocation = new FakeInvocation(invocation); - return newInvocation; -} - -/// An Invocation implementation that allows all attributes to be passed into -/// the constructor. -class FakeInvocation extends Invocation { +/// An Invocation implementation that takes arguments from [_typedArgs] and +/// [_typedNamedArgs]. +class _InvocationForTypedArguments extends Invocation { final Symbol memberName; final Map namedArguments; final List positionalArguments; @@ -77,9 +70,10 @@ class FakeInvocation extends Invocation { final bool isMethod; final bool isSetter; - factory FakeInvocation(Invocation invocation) { + factory _InvocationForTypedArguments(Invocation invocation) { if (_typedArgs.isEmpty && _typedNamedArgs.isEmpty) { - throw new StateError("FakeInvocation called when no typed calls have been saved."); + throw new StateError( + "_InvocationForTypedArguments called when no typed calls have been saved."); } // Handle named arguments first, so that we can provide useful errors for @@ -92,7 +86,7 @@ class FakeInvocation extends Invocation { _typedArgs.clear(); _typedNamedArgs.clear(); - return new FakeInvocation._( + return new _InvocationForTypedArguments._( invocation.memberName, positionalArguments, namedArguments, @@ -101,9 +95,17 @@ class FakeInvocation extends Invocation { invocation.isSetter); } + // Reconstitutes the named arguments in an invocation from [_typedNamedArgs]. + // + // The namedArguments in [invocation] which are null should be represented + // by a stored value in [_typedNamedArgs]. The null presumably came from + // [typed]. static Map _reconstituteNamedArgs(Invocation invocation) { var namedArguments = {}; var _typedNamedArgSymbols = _typedNamedArgs.keys.map((name) => new Symbol(name)); + + // Iterate through [invocation]'s named args, validate them, and add them + // to the return map. invocation.namedArguments.forEach((name, arg) { if (arg == null) { if (!_typedNamedArgSymbols.contains(name)) { @@ -111,9 +113,9 @@ class FakeInvocation extends Invocation { // `when(obj.fn(a: typed(any)))`. throw new ArgumentError( 'A typed argument was passed in as a named argument named "$name", ' - 'but did not a value for its name. Each typed argument that is ' - 'passed as a named argument needs to specify the `name` argument. ' - 'For example: `when(obj.fn(x: typed(any, name: "x")))`.'); + 'but did not pass a value for `named`. Each typed argument that is ' + 'passed as a named argument needs to specify the `named` argument. ' + 'For example: `when(obj.fn(x: typed(any, named: "x")))`.'); } } else { // Add each real named argument that was _not_ passed with [typed]. @@ -121,22 +123,24 @@ class FakeInvocation extends Invocation { } }); + // Iterate through the stored named args (stored with [typed]), validate + // them, and add them to the return map. _typedNamedArgs.forEach((name, arg) { Symbol nameSymbol = new Symbol(name); if (!invocation.namedArguments.containsKey(nameSymbol)) { - // Incorrect usage of [name], something like: - // `when(obj.fn(typed(any, name: 'a')))`. throw new ArgumentError( - 'A typed argument was declared with name $name, but was not passed ' - 'as an argument named $name.'); + 'A typed argument was declared as named $name, but was not passed ' + 'as an argument named $name.\n\n' + 'BAD: when(obj.fn(typed(any, named: "a")))\n' + 'GOOD: when(obj.fn(a: typed(any, named: "a")))'); } if (invocation.namedArguments[nameSymbol] != null) { - // Incorrect usage of [name], something like: - // `when(obj.fn(a: typed(any, name: 'b'), b: "string"))`. throw new ArgumentError( - 'A typed argument was declared with name $name, but a different ' + 'A typed argument was declared as named $name, but a different ' 'value (${invocation.namedArguments[nameSymbol]}) was passed as ' - '$name.'); + '$name.\n\n' + 'BAD: when(obj.fn(b: typed(any, name: "a")))\n' + 'GOOD: when(obj.fn(b: typed(any, name: "b")))'); } namedArguments[nameSymbol] = arg; }); @@ -153,31 +157,32 @@ class FakeInvocation extends Invocation { 'null arguments are not allowed alongside typed(); use ' '"typed(eq(null))"'); } - int i = 0; - int j = 0; - while (i < _typedArgs.length && j < invocation.positionalArguments.length) { - var arg = _typedArgs[i]; - if (invocation.positionalArguments[j] == null) { + int typedIndex = 0; + int positionalIndex = 0; + while (typedIndex < _typedArgs.length && + positionalIndex < invocation.positionalArguments.length) { + var arg = _typedArgs[typedIndex]; + if (invocation.positionalArguments[positionalIndex] == null) { // [typed] was used; add the [_ArgMatcher] given to [typed]. positionalArguments.add(arg); - i++; - j++; + typedIndex++; + positionalIndex++; } else { // [typed] was not used; add the [_ArgMatcher] from [invocation]. - positionalArguments.add(invocation.positionalArguments[j]); - j++; + positionalArguments.add(invocation.positionalArguments[positionalIndex]); + positionalIndex++; } } - while (j < invocation.positionalArguments.length) { + while (positionalIndex < invocation.positionalArguments.length) { // Some trailing non-[typed] arguments. - positionalArguments.add(invocation.positionalArguments[j]); - j++; + positionalArguments.add(invocation.positionalArguments[positionalIndex]); + positionalIndex++; } return positionalArguments; } - FakeInvocation._( + _InvocationForTypedArguments._( this.memberName, this.positionalArguments, this.namedArguments, @@ -431,11 +436,11 @@ get captureAny => new _ArgMatcher(anything, true); captureThat(Matcher matcher) => new _ArgMatcher(matcher, true); argThat(Matcher matcher) => new _ArgMatcher(matcher, false); -/*=T*/ typed/**/(_ArgMatcher matcher, {String name}) { - if (name == null) { +/*=T*/ typed/**/(_ArgMatcher matcher, {String named}) { + if (named == null) { _typedArgs.add(matcher); } else { - _typedNamedArgs[name] = matcher; + _typedNamedArgs[named] = matcher; } return null; } diff --git a/pubspec.yaml b/pubspec.yaml index 4cdcff5e..be8576c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: mockito -version: 0.11.1 +version: 1.0.0 author: Dmitriy Fibulwinter description: A mock framework inspired by Mockito. homepage: https://github.com/fibulwinter/dart-mockito diff --git a/test/mockito_test.dart b/test/mockito_test.dart index 461643f6..fe20df24 100644 --- a/test/mockito_test.dart +++ b/test/mockito_test.dart @@ -11,11 +11,11 @@ class RealClass { String methodWithObjArgs(RealClass x) => "Real"; // "SpecialArgs" here means type-parameterized args. But that makes for a long // method name. - String methodWithSpecialArgs( + String typeParameterizedFn( List w, List x, [List y, List z]) => "Real"; // "SpecialNamedArgs" here means type-parameterized, named args. But that // makes for a long method name. - String methodWithSpecialNamedArgs(List w, List x, {List y, List z}) => + String typeParameterizedNamedFn(List w, List x, {List y, List z}) => "Real"; String get getter => "Real"; void set setter(String arg) { @@ -219,93 +219,78 @@ void main() { expect(mock.methodWithNormalArgs(43), equals("43")); }); test("should mock method with typed arg matchers", () { - when(mock.methodWithSpecialArgs( - typed/*>*/(any), typed/*>*/(any))) + when(mock.typeParameterizedFn(typed(any), typed(any))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialArgs([42], [43]), equals("A lot!")); - expect(mock.methodWithSpecialArgs([43], [44]), equals("A lot!")); + expect(mock.typeParameterizedFn([42], [43]), equals("A lot!")); + expect(mock.typeParameterizedFn([43], [44]), equals("A lot!")); }); test("should mock method with an optional typed arg matcher", () { - when(mock.methodWithSpecialArgs( - typed/*>*/(any), - typed/*>*/(any), - typed/*>*/(any))) + when(mock.typeParameterizedFn(typed(any), typed(any), typed(any))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialArgs([42], [43], [44]), equals("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", () { - when(mock.methodWithSpecialArgs( - typed/*>*/(any), - typed/*>*/(any), - [44], - typed/*>*/(any))) + when(mock.typeParameterizedFn(typed(any), typed(any), [44], typed(any))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialArgs([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.methodWithSpecialArgs( - typed/*>*/(any), [43], typed/*>*/(any))) + when(mock.typeParameterizedFn(typed(any), [43], typed(any))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialArgs([42], [43], [44]), equals("A lot!")); - when(mock.methodWithSpecialArgs(typed/*>*/(any), [43])) + expect(mock.typeParameterizedFn([42], [43], [44]), equals("A lot!")); + when(mock.typeParameterizedFn(typed(any), [43])) .thenReturn("A bunch!"); - expect(mock.methodWithSpecialArgs([42], [43]), equals("A bunch!")); + expect(mock.typeParameterizedFn([42], [43]), equals("A bunch!")); }); test("should throw when [typed] used alongside [null].", () { - expect(() => when(mock.methodWithSpecialArgs( - typed/*>*/(any), null, typed/*>*/(any))), + expect(() => when(mock.typeParameterizedFn(typed(any), null, typed(any))), throwsArgumentError); - expect(() => when(mock.methodWithSpecialArgs( - typed/*>*/(any), typed/*>*/(any), null)), + expect(() => when(mock.typeParameterizedFn(typed(any), typed(any), null)), throwsArgumentError); }); test("should mock method when [typed] used alongside matched [null].", () { - when(mock.methodWithSpecialArgs( - typed/*>*/(any), argThat(equals(null)), typed/*>*/(any))) + when(mock.typeParameterizedFn( + typed(any), argThat(equals(null)), typed(any))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialArgs([42], null, [44]), equals("A lot!")); + expect(mock.typeParameterizedFn([42], null, [44]), equals("A lot!")); }); test("should mock method with named, typed arg matcher", () { - when(mock.methodWithSpecialNamedArgs( - typed/*>*/(any), [43], y: typed/*>*/(any, name: "y"))) + when(mock.typeParameterizedNamedFn( + typed(any), [43], y: typed(any, named: "y"))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialNamedArgs([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.methodWithSpecialNamedArgs( - typed/*>*/(any), - [43], - y: typed/*>*/(any, name: "y"), - z: argThat(contains(45)))) + mock.typeParameterizedNamedFn( + typed(any), [43], + y: typed(any, named: "y"), z: argThat(contains(45)))) .thenReturn("A lot!"); - expect(mock.methodWithSpecialNamedArgs([42], [43], y: [44], z: [45]), + 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.methodWithSpecialNamedArgs( - typed/*>*/(any), - [43], - y: typed/*>*/(any, name: "y"), - z: [45])) + mock.typeParameterizedNamedFn( + typed(any), [43], + y: typed(any, named: "y"), z: [45])) .thenReturn("A lot!"); - expect(mock.methodWithSpecialNamedArgs([42], [43], y: [44], z: [45]), + expect(mock.typeParameterizedNamedFn([42], [43], y: [44], z: [45]), equals("A lot!")); }); - test("should throw when [typed] used as a named arg, without `name:`", () { - expect(() => when(mock.methodWithSpecialNamedArgs( - typed/*>*/(any), [43], y: typed/*>*/(any))), + test("should throw when [typed] used as a named arg, without `named:`", () { + expect(() => when(mock.typeParameterizedNamedFn( + typed(any), [43], y: typed(any))), throwsArgumentError); }); - test("should throw when [typed] used as a positional arg, with `name:`", () { - expect(() => when(mock.methodWithSpecialNamedArgs( - typed/*>*/(any), typed/*>*/(any, name: "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 `name:`", () { - expect(() => when(mock.methodWithSpecialNamedArgs( - typed/*>*/(any), [43], y: typed/*>*/(any, name: "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); }); }); @@ -424,15 +409,14 @@ void main() { verify(mock.setter = "A"); }); test("should verify method with typed arg matchers", () { - mock.methodWithSpecialArgs([42], [43]); - verify(mock.methodWithSpecialArgs( - typed/*>*/(any), typed/*>*/(any))); + mock.typeParameterizedFn([42], [43]); + verify(mock.typeParameterizedFn(typed(any), typed(any))); }); test("should verify method with argument capturer", () { - mock.methodWithSpecialArgs([50], [17]); - mock.methodWithSpecialArgs([100], [17]); - expect(verify(mock.methodWithSpecialArgs( - typed/*>*/(captureAny), [17])).captured, + mock.typeParameterizedFn([50], [17]); + mock.typeParameterizedFn([100], [17]); + expect(verify(mock.typeParameterizedFn( + typed(captureAny), [17])).captured, equals([[50], [100]])); }); });