OLD | NEW |
| (Empty) |
1 library typed_mock; | |
2 | |
3 | |
4 _InvocationMatcher _lastMatcher; | |
5 | |
6 | |
7 /// Enables stubbing methods. | |
8 /// Use it when you want the mock to return particular value when particular | |
9 /// method is called. | |
10 Behavior when(_ignored) { | |
11 try { | |
12 var mock = _lastMatcher._mock; | |
13 mock._removeLastInvocation(); | |
14 // set behavior | |
15 var behavior = new Behavior._(_lastMatcher); | |
16 _lastMatcher._behavior = behavior; | |
17 return behavior; | |
18 } finally { | |
19 // clear to prevent memory leak | |
20 _lastMatcher = null; | |
21 } | |
22 } | |
23 | |
24 /// Verifies certain behavior happened a specified number of times. | |
25 Verifier verify(_ignored) { | |
26 try { | |
27 var mock = _lastMatcher._mock; | |
28 mock._removeLastInvocation(); | |
29 // set verifier | |
30 return new Verifier._(mock, _lastMatcher); | |
31 } finally { | |
32 // clear to prevent memory leak | |
33 _lastMatcher = null; | |
34 } | |
35 } | |
36 | |
37 | |
38 /// Verifies that the given mock doesn't have any unverified interaction. | |
39 void verifyNoMoreInteractions(TypedMock mock) { | |
40 var notVerified = mock._computeNotVerifiedInvocations(); | |
41 // OK | |
42 if (notVerified.isEmpty) { | |
43 return; | |
44 } | |
45 // fail | |
46 var invocationsString = _getInvocationsString(notVerified); | |
47 throw new VerifyError('Unexpected interactions:\n$invocationsString'); | |
48 } | |
49 | |
50 | |
51 /// Verifies that no interactions happened on the given mock. | |
52 void verifyZeroInteractions(TypedMock mock) { | |
53 var invocations = mock._invocations; | |
54 // OK | |
55 if (invocations.isEmpty) { | |
56 return; | |
57 } | |
58 // fail | |
59 var invocationsString = _getInvocationsString(invocations); | |
60 throw new VerifyError('Unexpected interactions:\n$invocationsString'); | |
61 } | |
62 | |
63 | |
64 /// [VerifyError] is thrown when one of the [verify] checks fails. | |
65 class VerifyError { | |
66 final String message; | |
67 VerifyError(this.message); | |
68 String toString() => 'VerifyError: $message'; | |
69 } | |
70 | |
71 | |
72 String _getInvocationsString(Iterable<Invocation> invocations) { | |
73 var buffer = new StringBuffer(); | |
74 invocations.forEach((invocation) { | |
75 var member = invocation.memberName; | |
76 buffer.write(member); | |
77 buffer.write(' '); | |
78 buffer.write(invocation.positionalArguments); | |
79 buffer.write(' '); | |
80 buffer.write(invocation.namedArguments); | |
81 buffer.writeln(); | |
82 }); | |
83 return buffer.toString(); | |
84 } | |
85 | |
86 | |
87 class _InvocationMatcher { | |
88 final Symbol _member; | |
89 final TypedMock _mock; | |
90 final List<ArgumentMatcher> _matchers = []; | |
91 | |
92 Behavior _behavior; | |
93 | |
94 _InvocationMatcher(this._mock, this._member, Invocation invocation) { | |
95 invocation.positionalArguments.forEach((argument) { | |
96 ArgumentMatcher matcher; | |
97 if (argument is ArgumentMatcher) { | |
98 matcher = argument; | |
99 } else { | |
100 matcher = equals(argument); | |
101 } | |
102 _matchers.add(matcher); | |
103 }); | |
104 } | |
105 | |
106 bool match(Invocation invocation) { | |
107 var arguments = invocation.positionalArguments; | |
108 if (arguments.length != _matchers.length) { | |
109 return false; | |
110 } | |
111 for (int i = 0; i < _matchers.length; i++) { | |
112 var matcher = _matchers[i]; | |
113 var argument = arguments[i]; | |
114 if (!matcher.match(argument)) { | |
115 return false; | |
116 } | |
117 } | |
118 return true; | |
119 } | |
120 } | |
121 | |
122 | |
123 class Behavior { | |
124 final _InvocationMatcher _matcher; | |
125 | |
126 Behavior._(this._matcher); | |
127 | |
128 bool _thenFunctionEnabled = false; | |
129 Function _thenFunction; | |
130 | |
131 bool _returnAlwaysEnabled = false; | |
132 var _returnAlways; | |
133 | |
134 bool _returnListEnabled = false; | |
135 List _returnList; | |
136 int _returnListIndex; | |
137 | |
138 bool _throwExceptionEnabled = false; | |
139 var _throwException; | |
140 | |
141 Behavior thenInvoke(Function function) { | |
142 _reset(); | |
143 _thenFunctionEnabled = true; | |
144 _thenFunction = function; | |
145 return this; | |
146 } | |
147 | |
148 Behavior thenReturn(value) { | |
149 _reset(); | |
150 _returnAlwaysEnabled = true; | |
151 _returnAlways = value; | |
152 return this; | |
153 } | |
154 | |
155 Behavior thenReturnList(List list) { | |
156 _reset(); | |
157 _returnListEnabled = true; | |
158 _returnList = list; | |
159 _returnListIndex = 0; | |
160 return this; | |
161 } | |
162 | |
163 Behavior thenThrow(exception) { | |
164 _reset(); | |
165 _throwExceptionEnabled = true; | |
166 _throwException = exception; | |
167 return this; | |
168 } | |
169 | |
170 _reset() { | |
171 _thenFunctionEnabled = false; | |
172 _returnAlwaysEnabled = false; | |
173 _returnListEnabled = false; | |
174 _throwExceptionEnabled = false; | |
175 } | |
176 | |
177 dynamic _getReturnValue(Invocation invocation) { | |
178 // function | |
179 if (_thenFunctionEnabled) { | |
180 return Function.apply(_thenFunction, invocation.positionalArguments, | |
181 invocation.namedArguments); | |
182 } | |
183 // always | |
184 if (_returnAlwaysEnabled) { | |
185 return _returnAlways; | |
186 } | |
187 // list | |
188 if (_returnListEnabled) { | |
189 if (_returnListIndex >= _returnList.length) { | |
190 throw new StateError('All ${_returnList.length} elements for ' | |
191 '${_matcher._member} from $_returnList have been exhausted.'); | |
192 } | |
193 return _returnList[_returnListIndex++]; | |
194 } | |
195 // exception | |
196 if (_throwExceptionEnabled) { | |
197 throw _throwException; | |
198 } | |
199 // no value | |
200 return null; | |
201 } | |
202 } | |
203 | |
204 | |
205 class Verifier { | |
206 final TypedMock _mock; | |
207 final _InvocationMatcher _matcher; | |
208 | |
209 Verifier._(this._mock, this._matcher); | |
210 | |
211 /// Marks matching interactions as verified and never fails. | |
212 void any() { | |
213 // mark as verified, but don't check the actual count | |
214 _count(); | |
215 } | |
216 | |
217 /// Verifies that there was no matching interactions. | |
218 void never() { | |
219 times(0); | |
220 } | |
221 | |
222 /// Verifies that there was excatly one martching interaction. | |
223 void once() { | |
224 times(1); | |
225 } | |
226 | |
227 /// Verifies that there was the specified number of matching interactions. | |
228 void times(int expected) { | |
229 var times = _count(); | |
230 if (times != expected) { | |
231 var member = _matcher._member; | |
232 throw new VerifyError('$expected expected, but $times' | |
233 ' invocations of $member recorded.'); | |
234 } | |
235 } | |
236 | |
237 /// Verifies that there was at least the specified number of matching | |
238 /// interactions. | |
239 void atLeast(int expected) { | |
240 var times = _count(); | |
241 if (times < expected) { | |
242 var member = _matcher._member; | |
243 throw new VerifyError('At least $expected expected, but only $times' | |
244 ' invocations of $member recorded.'); | |
245 } | |
246 } | |
247 | |
248 /// Verifies that there was at least one matching interaction. | |
249 void atLeastOnce() { | |
250 var times = _count(); | |
251 if (times == 0) { | |
252 var member = _matcher._member; | |
253 throw new VerifyError('At least one expected, but only zero' | |
254 ' invocations of $member recorded.'); | |
255 } | |
256 } | |
257 | |
258 /// Verifies that there was at most the specified number of matching | |
259 /// interactions. | |
260 void atMost(int expected) { | |
261 var times = _count(); | |
262 if (times > expected) { | |
263 var member = _matcher._member; | |
264 throw new VerifyError('At most $expected expected, but $times' | |
265 ' invocations of $member recorded.'); | |
266 } | |
267 } | |
268 | |
269 int _count() { | |
270 var times = 0; | |
271 _mock._invocations.forEach((invocation) { | |
272 if (invocation.memberName != _matcher._member) { | |
273 return; | |
274 } | |
275 if (!_matcher.match(invocation)) { | |
276 return; | |
277 } | |
278 _mock._verifiedInvocations.add(invocation); | |
279 times++; | |
280 }); | |
281 return times; | |
282 } | |
283 } | |
284 | |
285 | |
286 class TypedMock { | |
287 final Map<Symbol, List<_InvocationMatcher>> _matchersMap = {}; | |
288 | |
289 final List<Invocation> _invocations = []; | |
290 final Set<Invocation> _verifiedInvocations = new Set<Invocation>(); | |
291 | |
292 noSuchMethod(Invocation invocation) { | |
293 _invocations.add(invocation); | |
294 var member = invocation.memberName; | |
295 // prepare invocation matchers | |
296 var matchers = _matchersMap[member]; | |
297 if (matchers == null) { | |
298 matchers = []; | |
299 _matchersMap[member] = matchers; | |
300 } | |
301 // check if there is a matcher | |
302 for (var matcher in matchers) { | |
303 if (matcher.match(invocation)) { | |
304 _lastMatcher = matcher; | |
305 // generate value if there is a behavior | |
306 if (matcher._behavior != null) { | |
307 return matcher._behavior._getReturnValue(invocation); | |
308 } | |
309 // probably verification | |
310 return null; | |
311 } | |
312 } | |
313 // add a new matcher | |
314 var matcher = new _InvocationMatcher(this, member, invocation); | |
315 matchers.add(matcher); | |
316 _lastMatcher = matcher; | |
317 } | |
318 | |
319 Iterable<Invocation> _computeNotVerifiedInvocations() { | |
320 notVerified(e) => !_verifiedInvocations.contains(e); | |
321 return _invocations.where(notVerified); | |
322 } | |
323 | |
324 void _removeLastInvocation() { | |
325 _invocations.removeLast(); | |
326 } | |
327 } | |
328 | |
329 | |
330 abstract class ArgumentMatcher { | |
331 bool match(val); | |
332 } | |
333 | |
334 | |
335 class _ArgumentMatcher_equals extends ArgumentMatcher { | |
336 final expected; | |
337 | |
338 _ArgumentMatcher_equals(this.expected); | |
339 | |
340 @override | |
341 bool match(val) { | |
342 return val == expected; | |
343 } | |
344 } | |
345 | |
346 equals(expected) { | |
347 return new _ArgumentMatcher_equals(expected); | |
348 } | |
349 | |
350 | |
351 class _ArgumentMatcher_anyBool extends ArgumentMatcher { | |
352 @override | |
353 bool match(val) { | |
354 return val is bool; | |
355 } | |
356 } | |
357 | |
358 final anyBool = new _ArgumentMatcher_anyBool(); | |
359 | |
360 | |
361 class _ArgumentMatcher_anyInt extends ArgumentMatcher { | |
362 @override | |
363 bool match(val) { | |
364 return val is int; | |
365 } | |
366 } | |
367 | |
368 final anyInt = new _ArgumentMatcher_anyInt(); | |
369 | |
370 | |
371 class _ArgumentMatcher_anyObject extends ArgumentMatcher { | |
372 @override | |
373 bool match(val) { | |
374 return true; | |
375 } | |
376 } | |
377 | |
378 final anyObject = new _ArgumentMatcher_anyObject(); | |
379 | |
380 | |
381 class _ArgumentMatcher_anyString extends ArgumentMatcher { | |
382 @override | |
383 bool match(val) { | |
384 return val is String; | |
385 } | |
386 } | |
387 | |
388 final anyString = new _ArgumentMatcher_anyString(); | |
OLD | NEW |