Skip to content

Commit 935aea1

Browse files
committed
Add strong mode-compliant 'typed' API
1 parent aa9e03b commit 935aea1

File tree

3 files changed

+173
-4
lines changed

3 files changed

+173
-4
lines changed

README.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ verifyNoMoreInteractions(cat);
126126
```dart
127127
//simple capture
128128
cat.eatFood("Fish");
129-
expect(verify(cat.eatFood(capture)).captured.single, "Fish");
129+
expect(verify(cat.eatFood(captureAny)).captured.single, "Fish");
130130
//capture multiple calls
131131
cat.eatFood("Milk");
132132
cat.eatFood("Fish");
133-
expect(verify(cat.eatFood(capture)).captured, ["Milk", "Fish"]);
133+
expect(verify(cat.eatFood(captureAny)).captured, ["Milk", "Fish"]);
134134
//conditional capture
135135
cat.eatFood("Milk");
136136
cat.eatFood("Fish");
@@ -147,6 +147,77 @@ expect(cat.sound(), "Purr");
147147
//using real object
148148
expect(cat.lives, 9);
149149
```
150+
151+
## Strong mode compliance
152+
153+
Unfortunately, the use of the arg matchers in mock method calls (like `cat.eatFood(any)`)
154+
violates the Strong mode type system. Specifically, if the method signature of a mocked
155+
method has a parameter with a parameterized type (like `List<int>`), then passing `any` or
156+
`argThat` will result in a Strong mode warning:
157+
158+
> [warning] Unsound implicit cast from dynamic to List<int>
159+
160+
In order to write Strong mode-compliant tests with Mockito, you might need to use `typed`,
161+
annotating it with a type parameter comment. Let's use a slightly different `Cat` class to
162+
show some examples:
163+
164+
```dart
165+
class Cat {
166+
bool eatFood(List<String> foods, [List<String> mixins]) => true;
167+
int walk(List<String> places, {Map<String, String> gaits}) => 0;
168+
}
169+
170+
class MockCat extends Mock implements Cat {}
171+
172+
var cat = new MockCat();
173+
```
174+
175+
OK, what if we try to stub using `any`:
176+
177+
```dart
178+
when(cat.eatFood(any)).thenReturn(true);
179+
```
180+
181+
Let's analyze this code:
182+
183+
```
184+
$ dartanalyzer --strong test/cat_test.dart
185+
Analyzing [lib/cat_test.dart]...
186+
[warning] Unsound implicit cast from dynamic to List<String> (test/cat_test.dart, line 12, col 20)
187+
1 warning found.
188+
```
189+
190+
This code is not Strong mode-compliant. Let's change it to use `typed`:
191+
192+
```dart
193+
when(cat.eatFood(typed/*<List<String>>*/(any)))
194+
```
195+
196+
```
197+
$ dartanalyzer --strong test/cat_test.dart
198+
Analyzing [lib/cat_test.dart]...
199+
No issues found
200+
```
201+
202+
Great! A little ugly, but it works. Here are some more examples:
203+
204+
```dart
205+
when(cat.eatFood(typed/*<List<String>>*/(any), typed/*<List<String>>*/(any)))
206+
.thenReturn(true);
207+
when(cat.eatFood(typed/*<List<String>>*/(argThat(isEmpty)))).thenReturn(true);
208+
```
209+
210+
Named args require one more component: `typed` needs to know what named argument it is
211+
being passed into:
212+
213+
```dart
214+
when(cat.walk(
215+
typed/*<List<String>>*/(any),
216+
gaits: typed/*<Map<String, String>>*/(any), name: 'gaits')).thenReturn(true);
217+
```
218+
219+
Note the `name` argument.
220+
150221
## How it works
151222
The basics of the `Mock` class are nothing special: It uses `noSuchMethod` to catch
152223
all method invocations, and returns the value that you have configured beforehand with

lib/mockito.dart

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ _WhenCall _whenCall = null;
1111
final List<_VerifyCall> _verifyCalls = <_VerifyCall>[];
1212
final _TimeStampProvider _timer = new _TimeStampProvider();
1313
final List _capturedArgs = [];
14+
final List<_ArgMatcher> _typedArgs = <_ArgMatcher>[];
15+
final Map<String, _ArgMatcher> _typedNamedArgs = <String, _ArgMatcher>{};
1416

1517
class Mock {
1618
final List<RealCall> _realCalls = <RealCall>[];
@@ -25,7 +27,18 @@ class Mock {
2527

2628
dynamic noSuchMethod(Invocation invocation) {
2729
if (_whenInProgress) {
28-
_whenCall = new _WhenCall(this, invocation);
30+
if (_typedArgs.isEmpty && _typedNamedArgs.isEmpty) {
31+
_whenCall = new _WhenCall(this, invocation);
32+
} else {
33+
try {
34+
_whenCall = new _WhenCall(this, new FakeInvocation(invocation));
35+
} catch (e) {
36+
print("Boo");
37+
} finally {
38+
_typedArgs.clear();
39+
_typedNamedArgs.clear();
40+
}
41+
}
2942
return null;
3043
} else if (_verificationInProgress) {
3144
_verifyCalls.add(new _VerifyCall(this, invocation));
@@ -48,6 +61,56 @@ class Mock {
4861
String toString() => _givenName != null ? _givenName : runtimeType.toString();
4962
}
5063

64+
class FakeInvocation extends Invocation {
65+
final bool isAccessor;
66+
final bool isGetter;
67+
final bool isMethod;
68+
final bool isSetter;
69+
final Symbol memberName;
70+
final Map<Symbol, dynamic> namedArguments;
71+
final List<dynamic> positionalArguments;
72+
73+
factory FakeInvocation(Invocation invocation) {
74+
if (_typedArgs.isEmpty && _typedNamedArgs.isEmpty) {
75+
throw 'What?';
76+
}
77+
var positionalArguments = <_ArgMatcher>[];
78+
var namedArguments = <Symbol, dynamic>{};
79+
if (_typedArgs.length != invocation.positionalArguments.length) {
80+
throw 'All positional args must be typed, but ${_typedArgs.length}, ${invocation.positionalArguments.length}';
81+
}
82+
_typedArgs.forEach((arg) {
83+
positionalArguments.add(arg);
84+
});
85+
86+
// TODO: this can maybe be relaxed.
87+
if (_typedNamedArgs.length != invocation.namedArguments.length) {
88+
throw 'All named args must be typed';
89+
}
90+
_typedNamedArgs.forEach((name, arg) {
91+
namedArguments[new Symbol(name)] = arg;
92+
});
93+
94+
return new FakeInvocation._(
95+
invocation.isAccessor,
96+
invocation.isGetter,
97+
invocation.isMethod,
98+
invocation.isSetter,
99+
invocation.memberName,
100+
namedArguments,
101+
positionalArguments);
102+
}
103+
104+
FakeInvocation._(
105+
this.isAccessor,
106+
this.isGetter,
107+
this.isMethod,
108+
this.isSetter,
109+
this.memberName,
110+
this.namedArguments,
111+
this.positionalArguments);
112+
}
113+
51114
named(var mock, {String name, int hashCode}) => mock
52115
.._givenName = name
53116
.._givenHashCode = hashCode;
@@ -291,6 +354,15 @@ get captureAny => new _ArgMatcher(anything, true);
291354
captureThat(Matcher matcher) => new _ArgMatcher(matcher, true);
292355
argThat(Matcher matcher) => new _ArgMatcher(matcher, false);
293356

357+
/*=T*/ typed/*<T>*/(_ArgMatcher matcher, {String name}) {
358+
if (name == null) {
359+
_typedArgs.add(matcher);
360+
} else {
361+
_typedNamedArgs[name] = matcher;
362+
}
363+
return null;
364+
}
365+
294366
class VerificationResult {
295367
List captured = [];
296368
int callCount;

test/mockito_test.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ class RealClass {
99
String methodWithNamedArgs(int x, {int y}) => "Real";
1010
String methodWithTwoNamedArgs(int x, {int y, int z}) => "Real";
1111
String methodWithObjArgs(RealClass x) => "Real";
12+
// "SpecialArgs" here means type-parameterized args. But that makes for a long
13+
// method name.
14+
String methodWithSpecialArgs(List<int> x, [List<int> y]) => "Real";
15+
// "SpecialNamedArgs" here means type-parameterized, named args. But that
16+
// makes for a long method name.
17+
String methodWithSpecialNamedArgs(List<int> x, {List<int> y}) => "Real";
1218
String get getter => "Real";
1319
void set setter(String arg) {
1420
throw new StateError("I must be mocked");
@@ -204,6 +210,24 @@ void main() {
204210
when(mock.methodWithNormalArgs(argThat(equals(42)))).thenReturn("42");
205211
expect(mock.methodWithNormalArgs(43), equals("43"));
206212
});
213+
test("should mock method with typed argument matcher", () {
214+
when(mock.methodWithSpecialArgs(typed/*<List<int>>*/(any)))
215+
.thenReturn("A lot!");
216+
expect(mock.methodWithSpecialArgs([42]), equals("A lot!"));
217+
expect(mock.methodWithSpecialArgs([43]), equals("A lot!"));
218+
});
219+
test("should mock method with two typed argument matcher", () {
220+
when(mock.methodWithSpecialArgs(
221+
typed/*<List<int>>*/(any), typed/*<List<int>>*/(any)))
222+
.thenReturn("A lot!");
223+
expect(mock.methodWithSpecialArgs([42], [43]), equals("A lot!"));
224+
});
225+
test("should mock method with named, typed argument matcher", () {
226+
when(mock.methodWithSpecialNamedArgs(
227+
typed/*<List<int>>*/(any), y: typed/*<List<int>>*/(any, name: 'y')))
228+
.thenReturn("A lot!");
229+
expect(mock.methodWithSpecialNamedArgs([42], y: [43]), equals("A lot!"));
230+
});
207231
});
208232

209233
group("verify()", () {
@@ -478,7 +502,9 @@ void main() {
478502
});
479503
test("should captureOut list arguments", () {
480504
mock.methodWithListArgs([42]);
481-
expect(verify(mock.methodWithListArgs(captureAny)).captured.single, equals([42]));
505+
expect(verify(
506+
mock.methodWithListArgs(captureAny)).captured.single,
507+
equals([42]));
482508
});
483509
test("should captureOut multiple arguments", () {
484510
mock.methodWithPositionalArgs(1, 2);

0 commit comments

Comments
 (0)