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

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