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

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
« no previous file with comments | « lib/unittest/expect.dart ('k') | lib/unittest/unittest.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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 String _mockingErrorFormatter(actual, Matcher matcher, String signature) {
46 var description = new StringDescription();
47 description.add('Expected ${signature} ').addDescriptionOf(matcher).
48 add('\n but: ');
49 matcher.describeMismatch(actual, description);
50 return description.toString();
51 }
52
53 /**
54 * The failure handler for the [expect()] calls that occur in [verify()]
55 * methods in the mock objects. This calls the real failure handler used
56 * by the unit test library after formatting the error message with
57 * the custom formatter.
58 */
59 class _MockFailureHandler implements FailureHandler {
60 FailureHandler proxy;
61 _MockFailureHandler(this.proxy);
62 void fail(String reason) {
63 proxy.fail(reason);
64 }
65 void failMatch(actual, Matcher matcher, String reason) {
66 proxy.fail(_mockingErrorFormatter(actual, matcher, reason));
67 }
68 }
69
70 _MockFailureHandler _mockFailureHandler = null;
71
72 /**
73 * [_noArg] is a sentinel value representing no argument.
74 */
75 final _noArg = const _Sentinel();
Siggi Cherem (dart-lang) 2012/06/26 23:58:27 then we can just the value '_sentinel' that is in
gram 2012/06/27 17:58:48 Yes, they are equivalent but I prefer this separat
76
77 /**
78 * The behavior of a method call in the mock library is specified
79 * with [BehaviorValue]s. A [BehaviorValue] has a [value] to throw
80 * or return (depending on whether [isThrow] is true or not, respectively),
81 * and can either be one-shot, multi-shot, or infinitely repeating,
82 * depending on the value of [count (1, greater than 1, or 0 respectively).
83 */
84 class BehaviorValue {
85 var value;
86 bool isThrow;
87 int count;
88 BehaviorValue(this.value, [this.count = 1, this.isThrow = false]);
89 }
90
91 /**
92 * A [CallMatcher] is a special matcher used to match method calls (i.e.
93 * a method name and set of arguments). It is not a [Matcher] like the
94 * unit test [Matcher], but instead represents a collection of [Matcher]s,
95 * one per argument, that will be applied to the parameters to decide if
96 * the method call is a match.
97 */
98 class CallMatcher {
99 String name;
100 List<Matcher> argMatchers;
101
102 CallMatcher(String method, [
103 arg0 = _noArg,
104 arg1 = _noArg,
105 arg2 = _noArg,
106 arg3 = _noArg,
107 arg4 = _noArg,
108 arg5 = _noArg,
109 arg6 = _noArg,
110 arg7 = _noArg,
111 arg8 = _noArg,
112 arg9 = _noArg]) {
113 name = method;
114 argMatchers = new List<Matcher>();
115 if (arg0 == _noArg) return;
116 argMatchers.add(wrapMatcher(arg0));
117 if (arg1 == _noArg) return;
118 argMatchers.add(wrapMatcher(arg1));
119 if (arg2 == _noArg) return;
120 argMatchers.add(wrapMatcher(arg2));
121 if (arg3 == _noArg) return;
122 argMatchers.add(wrapMatcher(arg3));
123 if (arg4 == _noArg) return;
124 argMatchers.add(wrapMatcher(arg4));
125 if (arg5 == _noArg) return;
126 argMatchers.add(wrapMatcher(arg5));
127 if (arg6 == _noArg) return;
128 argMatchers.add(wrapMatcher(arg6));
129 if (arg7 == _noArg) return;
130 argMatchers.add(wrapMatcher(arg7));
131 if (arg8 == _noArg) return;
132 argMatchers.add(wrapMatcher(arg8));
133 if (arg9 == _noArg) return;
134 argMatchers.add(wrapMatcher(arg9));
135 }
136
137 /**
138 * We keep our behavior specifications in a Map, which is keyed
139 * by the [CallMatcher]. To make the keys unique and to get a
140 * descriptive value for the [CallMatcher] we have this override
141 * of [toString()].
142 */
143 String toString() {
144 Description d = new StringDescription();
145 d.add(name).add('(');
146 for (var i = 0; i < argMatchers.length; i++) {
147 if (i > 0) d.add(', ');
148 d.addDescriptionOf(argMatchers[i]);
149 }
150 d.add(')');
151 return d.toString();
152 }
153
154 /**
155 * Given a [method] name oand list of [arguments], return true
156 * if it matches this [CallMatcher.
157 */
158 bool matches(String method, List arguments) {
159 if (method != this.name || arguments.length != argMatchers.length) {
160 return false;
161 }
162 for (var i = 0; i < arguments.length; i++) {
163 if (!argMatchers[i].matches(arguments[i])) {
164 return false;
165 }
166 }
167 return true;
168 }
169 }
170
171 /**
172 * A [Behavior] represents how a [Mock] will respond to one particular
173 * type of method call.
174 */
175 class Behavior {
176 CallMatcher matcher; // The method call matcher.
177 List<BehaviorValue> returnValues; // The values to return/throw.
178
179 Behavior (this.matcher) {
180 returnValues = new List<BehaviorValue>();
181 }
182
183 /**
184 * [thenReturn] creates a return value, that is returned [count]
185 * times (1 by default).
186 */
187 Behavior thenReturn(value, [count = 1]) {
188 returnValues.add(new BehaviorValue(value, count));
189 return this; // For chaining calls.
190 }
191
192 /** [alwaysReturn] creates a repeating return value. */
193 Behavior alwaysReturn(value) {
194 return thenReturn(value, 0);
195 }
196
197 /**
198 * [thenThrow] creates an exception, that is thrown [count]
199 * times (1 by default).
200 */
201 Behavior thenThrow(value, [count = 1]) {
202 returnValues.add(new BehaviorValue(value, count, true));
203 return this; // For chaining calls.
204 }
205
206 /** [alwaysThrow] creates a repeating exception. */
207 Behavior alwaysThrow(value) {
208 return thenThrow(value, 0);
209 }
210
211 /** [matches] return true if a method call matches the [Behavior]. */
212 bool matches(name, args) => matcher.matches(name, args);
213 }
214
215 /**
216 * Every call to a [Mock] object method is logged. The logs are
217 * kept in instances of [LogEntry].
218 */
219 class LogEntry {
220 final String name; // The method name.
221 final List args; // The parameters.
222 final BehaviorValue result; // The behavior that resulted.
223
224 const LogEntry(this.name, this.args, this.result);
225 }
226
227 /**
228 * We do verification on a list of [LogEntry]s. To allow chaining
229 * of calls to verify, we encapsulate such a list in the [LogEntryList]
230 * class.
231 */
232 class LogEntryList {
233 final String filter;
234 final List<LogEntry> logs;
235 const LogEntryList(this.logs, [this.filter = null]);
236
237 /** Add a [LogEntry] to the log. */
238 add(LogEntry entry) => logs.add(entry);
239
240 /**
241 * Create a new [LogEntryList] consisting of [LogEntry]s from
242 * this list that match the specified [logfilter].
243 */
244 LogEntryList getMatches(CallMatcher logfilter) {
245 LogEntryList rtn =
246 new LogEntryList(new List<LogEntry>(), logfilter.toString());
247 for (var i = 0; i < logs.length; i++) {
248 LogEntry entry = logs[i];
249 if (logfilter.matches(entry.name, entry.args)) {
250 rtn.add(entry);
251 }
252 }
253 return rtn;
254 }
255
256 /** Apply a unit test [Matcher] to the [LogEntryList]. */
257 LogEntryList verify(Matcher matcher) {
258 if (_mockFailureHandler == null) {
259 _mockFailureHandler =
260 new _MockFailureHandler(getOrCreateExpectFailureHandler());
261 }
262 expect(logs, matcher, filter, _mockFailureHandler);
263 return this;
264 }
265 }
266
267 /**
268 * [_TimesMatcher]s are used to make assertions about the number of
269 * times a method was called.
270 */
271 class _TimesMatcher extends BaseMatcher {
272 final int min, max;
273 const _TimesMatcher(this.min, [this.max = -1]);
274 bool matches(log) => log.length >= min && (max < 0 || log.length <= max);
275 Description describe(Description description) {
276 description.add(' to be called ');
277 if (max < 0) {
278 description.add('at least $min');
279 } else if (max == min) {
280 description.add('$max');
281 } else if (min == 0) {
282 description.add('at most $max');
283 } else {
284 description.add('between $min and $max');
285 }
286 return description.add(' times');
287 }
288 Description describeMismatch(log, Description mismatchDescription) =>
289 mismatchDescription.add('was called ${log.length} times');
290 }
291
292 /** [calledExactly] matches an exact number of calls. */
293 Matcher calledExactly(count) {
294 return new _TimesMatcher(count, count);
295 }
296
297 /** [calledAtLeast] matches a minimum number of calls. */
298 Matcher calledAtLeast(count) {
299 return new _TimesMatcher(count);
300 }
301
302 /** [calledAtMost] matches a maximum number of calls. */
303 Matcher calledAtMost(count) {
304 return new _TimesMatcher(0, count);
305 }
306
307 /** [neverCalled] matches zero calls. */
308 final Matcher neverCalled = const _TimesMatcher(0, 0);
309
310 /** [calledOnce] matches exactly one call. */
311 final Matcher calledOnce = const _TimesMatcher(1, 1);
312
313 /** [calledAtLeastOnce] matches one or more calls. */
314 final Matcher calledAtLeastOnce = const _TimesMatcher(1);
315
316 /** [calledAtMostOnce] matches zero or one call. */
317 final Matcher calledAtMostOnce = const _TimesMatcher(0, 1);
318
319 /** [Mock] is the base class for all mocked objects. */
320 class Mock {
321 Map<String,Behavior> behaviors; /** The set of [behavior]s supported. */
322 LogEntryList log; /** The [log] of calls made. */
323
324 Mock() {
325 behaviors = new Map<String,Behavior>();
326 log = new LogEntryList(new List<LogEntry>());
327 }
328
329 /**
330 * [when] is used to create a new or extend an existing [Behavior].
331 * The [method] name and the argument [Matcher] is specified. A
332 * corresponding [CallMatcher] is created, and the [Behavior]s for
333 * its signature are returned (being created first if needed).
334 */
335 Behavior when(String method, [
336 arg0 = _noArg,
337 arg1 = _noArg,
338 arg2 = _noArg,
339 arg3 = _noArg,
340 arg4 = _noArg,
341 arg5 = _noArg,
342 arg6 = _noArg,
343 arg7 = _noArg,
344 arg8 = _noArg,
345 arg9 = _noArg]) {
346 CallMatcher logfilter = new CallMatcher(method,
347 arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
348 String key = logfilter.toString();
349 if (!behaviors.containsKey(key)) {
350 Behavior b = new Behavior(logfilter);
351 behaviors[key] = b;
352 return b;
353 } else {
354 return behaviors[key];
355 }
356 }
357
358 /**
359 * This is the handler for method calls. We loo through the list
360 * of [Behavior]s, and find the first match that still has return
361 * values available, and then do the action specified by that
362 * return value. If we find no [Behavior] to apply an exception is
363 * thrown.
364 */
365 noSuchMethod(String name, List args) {
366 for (String k in behaviors.getKeys()) {
367 Behavior b = behaviors[k];
368 if (b.matches(name, args)) {
369 List rv = b.returnValues;
370 if (rv == null || rv.length == 0) {
371 continue; // No return values left in this Behavior.
372 }
373 // Get the first response.
374 BehaviorValue bv = rv[0];
375 // If it is exhausted, remove it from the list.
376 // Note that for endlessly repeating values, we started the count at
377 // 0, so we get a potentially useful value here, which is the
378 // (negation of) the number of times we returned the value.
379 if (--bv.count == 0) {
380 rv.removeRange(0, 1);
381 if (rv.length == 0) {
382 // Remove the behavior. Note that in the future there
383 // may be some value in preserving the behaviors for
384 // auditing purposes (e.g. how many times was this behavior used?).
385 // If we do decide to keep them and perf is an issue instead of
386 // deleting we could move this to a separate list.
387 behaviors.remove(k);
388 }
389 }
390 // Log the method call and the response.
391 log.add(new LogEntry(name, args, bv));
392 // Do the response.
393 if (bv.isThrow) {
394 throw bv.value;
395 } else {
396 return bv.value;
397 }
398 }
399 }
400 throw new Exception('No behavior specified for method $name');
401 }
402
403 /**
404 * [verify] extracts all calls from the object log that match the
405 * method signature, then applies the [was] matcher. The matching
406 * list of [LogEntry]s is returned so that further calls to verify()
407 * can be chained .
408 */
409 LogEntryList verify(String method, [ arg0 = _noArg,
410 arg1 = _noArg,
411 arg2 = _noArg,
412 arg3 = _noArg,
413 arg4 = _noArg,
414 arg5 = _noArg,
415 arg6 = _noArg,
416 arg7 = _noArg,
417 arg8 = _noArg,
418 arg9 = _noArg,
419 Matcher was = calledOnce]) {
420 CallMatcher logfilter = new CallMatcher(method,
421 arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
422 LogEntryList _logs = log.getMatches(logfilter);
423 _logs.verify(was);
424 return _logs;
425 }
426
427 /** [verifyZeroInteractions] returns true if no calls were made */
428 bool verifyZeroInteractions() => log.logs.length == 0;
429
430 }
431
OLDNEW
« no previous file with comments | « lib/unittest/expect.dart ('k') | lib/unittest/unittest.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698