Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(84)

Side by Side Diff: lib/unittest/mock.dart

Issue 10679005: Initial version of mocking support. There is still quite a bit of (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 /**
6 * Support for basic mocking.
7 *
8 * To create a mock objects for some class T, create a new class using:
9 *
10 * class MockT extends Mock implements T {};
11 *
12 * Then specify the behavior of the Mock for different methods using
13 * [when] (to select the method and parameters) and [thenReturn],
14 * [alwaysReturn], [thenThrow] and/or [alwaysThrow].
15 *
16 * You can then use the mock object. Once you are done, to verify the
17 * behavior, use [verify] to extract a relevant subset of method call
18 * logs and apply [Matchers] to these.
19 *
20 * Limitations:
21 * - only positional parameters are supported (up to 10);
22 * - to mock getters you will need to include parentheses.
23 *
24 * Here is a simple example:
25 *
26 * class MockList extends Mock implements List {};
27 *
28 * List m = new MockList();
29 * m.when('add', anything).alwaysReturn(0);
30 *
31 * m.add('foo');
32 * m.add('bar');
33 *
34 * m.verify('add', anything, was:calledExactly(2));
35 * m.verify('add', 'foo', was:calledOnce);
36 * m.verify('add', 'isNull, was:neverCalled);
37 */
38
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 maybe this should be it's own library? #library('m
gram 2012/06/26 23:39:59 I thought about that, but I think that as long as
Siggi Cherem (dart-lang) 2012/06/26 23:58:27 Then it would be nice if this documentation is att
39 /**
40 * The error formatter for mocking is a bit different from the default one
41 * for unit testing; instead of the third argument being a 'reason'
42 * it is instead a [signature] describing the method signature filter
43 * that was used to select the logs that were verified.
44 */
45
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 remove empty line
gram 2012/06/26 23:39:59 Done.
46 String _mockingErrorFormatter(actual, Matcher matcher, String signature) {
47 var description = new StringDescription();
48 description.add('Expected ${signature} ').addDescriptionOf(matcher).
49 add('\n but: ');
50 matcher.describeMismatch(actual, description);
51 return description.toString();
52 }
53
54 /**
55 * The failure handler for the [expect()] calls that occur in [verify()]
56 * methods in the mock objects. This calls the real failure handler used
57 * by the unit test library after formatting the error message with
58 * the custom formatter.
59 */
60 class _MockFailureHandler implements FailureHandler {
61 FailureHandler proxy;
62 _MockFailureHandler(this.proxy);
63 void fail(String reason) {
64 proxy.fail(reason);
65 }
66 void failMatch(actual, Matcher matcher, String reason) {
67 proxy.fail(_mockingErrorFormatter(actual, matcher, reason));
68 }
69 }
70
71 _MockFailureHandler _mockFailureHandler = null;
72
73 /**
74 * [_NoArg] is a class with an instance used to fake varargs.
75 */
76 class _NoArg {
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 we are also doing the same thing for _SpreadArgsHe
gram 2012/06/26 23:39:59 Done.
77 const _NoArg();
78 String toString() => '';
79 }
80
81 /**
82 * [noArg] is a sentinel value representing no argument.
83 */
84 final noArg = const _NoArg();
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 => _noArg (make noArg private to this library.)
gram 2012/06/26 23:39:59 Done.
85
86 /**
87 * The behavior of a method call in the mock library is specified
88 * with [BehaviorValue]s. A [BehaviorValue] has a [value] to throw
89 * or return (depending on whether [isThrow] is true or not, respectively),
90 * and can either be one-shot or repeating, depending on the value of
91 * of [isOneShot].
92 */
93 class BehaviorValue {
94 var value;
95 bool isThrow;
96 bool isOneShot;
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 maybe instead of 1 vs many, could we specify a num
gram 2012/06/26 23:39:59 Done.
97 BehaviorValue(this.value, [this.isOneShot = true, this.isThrow = false]);
98 }
99
100 /**
101 * A [CallMatcher] is a special matcher used to match method calls (i.e.
102 * a method name and set of arguments). It is not a [Matcher] like the
103 * unit test [Matcher], but instead represents a collection of [Matcher]s,
104 * one per argument, that will be applied to the parameters to decide if
105 * the method call is a match.
106 */
107 class CallMatcher {
108 String name;
109 List<Matcher> argMatchers;
110
111 CallMatcher(String method, [
112 arg0 = noArg,
113 arg1 = noArg,
114 arg2 = noArg,
115 arg3 = noArg,
116 arg4 = noArg,
117 arg5 = noArg,
118 arg6 = noArg,
119 arg7 = noArg,
120 arg8 = noArg,
121 arg9 = noArg]) {
122 name = method;
123 argMatchers = new List<Matcher>();
124 if (arg0 != noArg) {
125 argMatchers.add(wrapMatcher(arg0));
126 if (arg1 != noArg) {
127 argMatchers.add(wrapMatcher(arg1));
128 if (arg2 != noArg) {
129 argMatchers.add(wrapMatcher(arg2));
130 if (arg3 != noArg) {
131 argMatchers.add(wrapMatcher(arg3));
132 if (arg4 != noArg) {
133 argMatchers.add(wrapMatcher(arg4));
134 if (arg5 != noArg) {
135 argMatchers.add(wrapMatcher(arg5));
136 if (arg6 != noArg) {
137 argMatchers.add(wrapMatcher(arg6));
138 if (arg7 != noArg) {
139 argMatchers.add(wrapMatcher(arg7));
140 if (arg8 != noArg) {
141 argMatchers.add(wrapMatcher(arg8));
142 if (arg9 != noArg) {
143 argMatchers.add(wrapMatcher(arg9));
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 style nit: pine trees!! please fix
gram 2012/06/26 23:39:59 Done.
144 }
145 }
146 }
147 }
148 }
149 }
150 }
151 }
152 }
153 }
154 }
155
156 /**
157 * We keep our behavior specifications in a Map, which is keyed
158 * by the [CallMatcher]. To make the keys unique and to get a
159 * descriptive value for the [CallMatcher] we have this override
160 * of [toString()].
161 */
162 String toString() {
163 Description d = new StringDescription();
164 d.add(name).add('(');
165 for (var i = 0; i < argMatchers.length; i++) {
166 if (i > 0) d.add(', ');
167 d.addDescriptionOf(argMatchers[i]);
168 }
169 d.add(')');
170 return d.toString();
171 }
172
173 /**
174 * Given a [method] name oand list of [arguments], return true
175 * if it matches this [CallMatcher.
176 */
177 bool matches(String method, List arguments) {
178 if (method != this.name || arguments.length != argMatchers.length) {
179 return false;
180 }
181 for (var i = 0; i < arguments.length; i++) {
182 if (!argMatchers[i].matches(arguments[i])) {
183 return false;
184 }
185 }
186 return true;
187 }
188 }
189
190 /**
191 * A [Behavior] represents how a [Mock] will respond to one particular
192 * type of method call.
193 */
194 class Behavior {
195 CallMatcher matcher; // The method call matcher.
196 List<BehaviorValue> returnValues; // The values to return/throw.
197
198 Behavior (this.matcher) {
199 returnValues = new List<BehaviorValue>();
200 }
201
202 /** [thenReturn] creates a one-shot return value. */
203 Behavior thenReturn(value) {
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 => consider adding a count parameter ?
gram 2012/06/26 23:39:59 Done.
204 returnValues.add(new BehaviorValue(value));
205 return this; // For chaining calls.
206 }
207
208 /** [alwaysReturn] creates a repeating return value. */
209 Behavior alwaysReturn(value) {
210 returnValues.add(new BehaviorValue(value, false));
211 return this; // For chaining calls.
212 }
213
214 /** [thenThrow] creates a one-shot exception. */
215 Behavior thenThrow(value) {
216 returnValues.add(new BehaviorValue(value, true, true));
217 return this; // For chaining calls.
218 }
219
220 /** [alwaysThrow] creates a repeating exception. */
221 Behavior alwaysThrow(value) {
222 returnValues.add(new BehaviorValue(value, false, true));
223 return this; // For chaining calls.
224 }
225
226 /** [matches] return true if a method call matches the [Behavior]. */
227 bool matches(name, args) => matcher.matches(name, args);
228 }
229
230 /**
231 * Every call to a [Mock] object method is logged. The logs are
232 * kept in instances of [LogEntry].
233 */
234 class LogEntry {
235 final String name; // The method name.
236 final List args; // The parameters.
237 final BehaviorValue result; // The behavior that resulted.
238
239 const LogEntry(this.name, this.args, this.result);
240 }
241
242 /**
243 * We do verification on a list of [LogEntry]s. To allow chaining
244 * of calls to verify, we encapsulate such a list in the [LogEntryList]
245 * class.
246 */
247 class LogEntryList {
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 implements/extends List<LogEntry>?
gram 2012/06/26 23:39:59 You can't use extends like that, only implements,
248 final String filter;
249 final List<LogEntry> logs;
250 const LogEntryList(this.logs, [this.filter = null]);
251
252 /** Add a [LogEntry] to the log. */
253 void add(LogEntry entry) {
254 logs.add(entry);
255 }
256
257 /**
258 * Create a new [LogEntryList] consisting of [LogEntry]s from
259 * this list that match the specified [logfilter].
260 */
261 LogEntryList getMatches(CallMatcher logfilter) {
262 LogEntryList rtn =
263 new LogEntryList(new List<LogEntry>(), logfilter.toString());
264 for (var i = 0; i < logs.length; i++) {
265 LogEntry entry = logs[i];
266 if (logfilter.matches(entry.name, entry.args)) {
267 rtn.add(entry);
268 }
269 }
270 return rtn;
271 }
272
273 /** Apply a unit test [Matcher] to the [LogEntryList]. */
274 LogEntryList verify(Matcher matcher) {
275 if (_mockFailureHandler == null) {
276 _mockFailureHandler = new _MockFailureHandler(getFailureHandler());
277 }
278 expect(logs, matcher, filter, _mockFailureHandler);
279 return this;
280 }
281 }
282
283 /**
284 * [_TimesMatcher]s are used to make assertions about the number of
285 * times a method was called.
286 */
287 class _TimesMatcher extends BaseMatcher {
288 final int min, max;
289 const _TimesMatcher(this.min, [this.max = -1]);
290 bool matches(log) => log.length >= min && (max < 0 || log.length <= max);
291 Description describe(Description description) {
292 description.add(' to be called ');
293 if (max < 0) {
294 description.add('at least $min');
295 } else if (max == min) {
296 description.add('$max');
297 } else if (min == 0) {
298 description.add('at most $max');
299 } else {
300 description.add('between $min and $max');
301 }
302 return description.add(' times');
303 }
304 Description describeMismatch(log, Description mismatchDescription) =>
305 mismatchDescription.add('was called ${log.length} times');
306 }
307
308 /** [calledExactly] matches an exact number of calls. */
309 Matcher calledExactly(count) {
310 return new _TimesMatcher(count, count);
311 }
312
313 /** [calledAtLeast] matches a minimum number of calls. */
314 Matcher calledAtLeast(count) {
315 return new _TimesMatcher(count);
316 }
317
318 /** [calledAtMost] matches a maximum number of calls. */
319 Matcher calledAtMost(count) {
320 return new _TimesMatcher(0, count);
321 }
322
323 /** [neverCalled] matches zero calls. */
324 final Matcher neverCalled = const _TimesMatcher(0, 0);
325
326 /** [calledOnce] matches exactly one call. */
327 final Matcher calledOnce = const _TimesMatcher(1, 1);
328
329 /** [calledAtLeastOnce] matches one or more calls. */
330 final Matcher calledAtLeastOnce = const _TimesMatcher(1);
331
332 /** [calledAtMostOnce] matches zero or one call. */
333 final Matcher calledAtMostOnce = const _TimesMatcher(0, 1);
334
335 /** [Mock] is the base class for all mocked objects. */
336 class Mock {
337 Map<String,Behavior> behaviors; /** The set of [behavior]s supported. */
338 LogEntryList log; /** The [log] of calls made. */
339
340 Mock() {
341 behaviors = new Map<String,Behavior>();
342 log = new LogEntryList(new List<LogEntry>());
343 }
344
345 /**
346 * [when] is used to create a new or extend an existing [Behavior].
347 * The [method] name and the argument [Matcher] is specified. A
348 * corresponding [CallMatcher] is created, and the [Behavior]s for
349 * its signature are returned (being created first if needed).
350 */
351 Behavior when(String method, [
352 arg0 = noArg,
353 arg1 = noArg,
354 arg2 = noArg,
355 arg3 = noArg,
356 arg4 = noArg,
357 arg5 = noArg,
358 arg6 = noArg,
359 arg7 = noArg,
360 arg8 = noArg,
361 arg9 = noArg]) {
362 CallMatcher logfilter = new CallMatcher(method,
363 arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
364 String key = logfilter.toString();
365 if (!behaviors.containsKey(key)) {
366 Behavior b = new Behavior(logfilter);
367 behaviors[key] = b;
368 return b;
369 } else {
370 return behaviors[key];
371 }
372 }
373
374 /**
375 * This is the handler for method calls. We loo through the list
376 * of [Behavior]s, and find the first match that still has return
377 * values available, and then do the action specified by that
378 * return value. If we find no [Behavior] to apply an exception is
379 * thrown.
380 */
381 noSuchMethod(String name, List args) {
382 for (Behavior b in behaviors.getValues()) {
383 if (b.matches(name, args)) {
384 List rv = b.returnValues;
385 if (rv == null || rv.length == 0) {
386 continue; // No return values left in this Behavior.
Siggi Cherem (dart-lang) 2012/06/26 21:35:44 should we do some cleanup of the behaviour list if
gram 2012/06/26 23:39:59 Maybe, maybe not. If perf was an issue we could mo
Siggi Cherem (dart-lang) 2012/06/26 23:58:27 Ok - I'm also not sure what are the performance im
387 }
388 // Get the first response.
389 BehaviorValue bv = rv[0];
390 // If it is one-shot, remove it from the list.
391 if (bv.isOneShot) {
392 rv.removeRange(0, 1);
393 }
394 // Log the method call and the response.
395 log.add(new LogEntry(name, args, bv));
396 // Do the response.
397 if (bv.isThrow) {
398 throw bv.value;
399 } else {
400 return bv.value;
401 }
402 }
403 }
404 throw new Exception('No behavior specified for method $name');
405 }
406
407 /**
408 * [verify] extracts all calls from the object log that match the
409 * method signature, then applies the [was] matcher. The matching
410 * list of [LogEntry]s is returned so that further calls to verify()
411 * can be chained .
412 */
413 LogEntryList verify(String method, [ arg0 = noArg,
414 arg1 = noArg,
415 arg2 = noArg,
416 arg3 = noArg,
417 arg4 = noArg,
418 arg5 = noArg,
419 arg6 = noArg,
420 arg7 = noArg,
421 arg8 = noArg,
422 arg9 = noArg,
423 Matcher was = calledOnce]) {
424 CallMatcher logfilter = new CallMatcher(method,
425 arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
426 LogEntryList _logs = log.getMatches(logfilter);
427 _logs.verify(was);
428 return _logs;
429 }
430
431 /** [verifyZeroInteractions] returns true if no calls were made */
432 bool verifyZeroInteractions() => log.logs.length == 0;
433
434 }
435
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698