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

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

Issue 10752007: Mocking library improvements: (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 | « no previous file | lib/unittest/operator_matchers.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 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 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 /** 5 /**
6 * The error formatter for mocking is a bit different from the default one 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' 7 * for unit testing; instead of the third argument being a 'reason'
8 * it is instead a [signature] describing the method signature filter 8 * it is instead a [signature] describing the method signature filter
9 * that was used to select the logs that were verified. 9 * that was used to select the logs that were verified.
10 */ 10 */
(...skipping 23 matching lines...) Expand all
34 } 34 }
35 35
36 _MockFailureHandler _mockFailureHandler = null; 36 _MockFailureHandler _mockFailureHandler = null;
37 37
38 /** 38 /**
39 * [_noArg] is a sentinel value representing no argument. 39 * [_noArg] is a sentinel value representing no argument.
40 */ 40 */
41 final _noArg = const _Sentinel(); 41 final _noArg = const _Sentinel();
42 42
43 /** The ways in which a call to a mock method can be handled. */ 43 /** The ways in which a call to a mock method can be handled. */
44 final RETURN = 0; 44 class _Action {
45 final THROW = 1; 45 /** Do nothing (void method) */
46 final PROXY = 2; 46 static final IGNORE = const _Action._('IGNORE');
47
48 /** Return a supplied value. */
49 static final RETURN = const _Action._('RETURN');
50
51 /** Throw a supplied value. */
52 static final THROW = const _Action._('THROW');
53
54 /** Call a supplied function. */
55 static final PROXY = const _Action._('PROXY');
56
57 const _Action._(this.name);
58
59 final String name;
60 }
47 61
48 /** 62 /**
49 * The behavior of a method call in the mock library is specified 63 * The behavior of a method call in the mock library is specified
50 * with [Responder]s. A [Responder] has a [value] to throw 64 * with [Responder]s. A [Responder] has a [value] to throw
51 * or return (depending on whether [isThrow] is true or not, respectively), 65 * or return (depending on whether [isThrow] is true or not, respectively),
52 * and can either be one-shot, multi-shot, or infinitely repeating, 66 * and can either be one-shot, multi-shot, or infinitely repeating,
53 * depending on the value of [count (1, greater than 1, or 0 respectively). 67 * depending on the value of [count (1, greater than 1, or 0 respectively).
54 */ 68 */
55 class Responder { 69 class Responder {
56 var value; 70 var value;
57 int action; 71 _Action action;
58 int count; 72 int count;
59 Responder(this.value, [this.count = 1, this.action = RETURN]); 73 Responder(this.value, [this.count = 1, this.action = _Action.RETURN]);
60 } 74 }
61 75
62 /** 76 /**
63 * A [CallMatcher] is a special matcher used to match method calls (i.e. 77 * A [CallMatcher] is a special matcher used to match method calls (i.e.
64 * a method name and set of arguments). It is not a [Matcher] like the 78 * a method name and set of arguments). It is not a [Matcher] like the
65 * unit test [Matcher], but instead represents a collection of [Matcher]s, 79 * unit test [Matcher], but instead represents a method name and a
66 * one per argument, that will be applied to the parameters to decide if 80 * collection of [Matcher]s, one per argument, that will be applied
67 * the method call is a match. 81 * to the parameters to decide if the method call is a match.
68 */ 82 */
69 class CallMatcher { 83 class CallMatcher {
70 String name; 84 String name;
71 List<Matcher> argMatchers; 85 List<Matcher> argMatchers;
72 86
73 CallMatcher(String method, [ 87 CallMatcher(this.name, [
74 arg0 = _noArg, 88 arg0 = _noArg,
75 arg1 = _noArg, 89 arg1 = _noArg,
76 arg2 = _noArg, 90 arg2 = _noArg,
77 arg3 = _noArg, 91 arg3 = _noArg,
78 arg4 = _noArg, 92 arg4 = _noArg,
79 arg5 = _noArg, 93 arg5 = _noArg,
80 arg6 = _noArg, 94 arg6 = _noArg,
81 arg7 = _noArg, 95 arg7 = _noArg,
82 arg8 = _noArg, 96 arg8 = _noArg,
83 arg9 = _noArg]) { 97 arg9 = _noArg]) {
84 name = method;
85 argMatchers = new List<Matcher>(); 98 argMatchers = new List<Matcher>();
86 if (arg0 == _noArg) return; 99 if (arg0 == _noArg) return;
87 argMatchers.add(wrapMatcher(arg0)); 100 argMatchers.add(wrapMatcher(arg0));
88 if (arg1 == _noArg) return; 101 if (arg1 == _noArg) return;
89 argMatchers.add(wrapMatcher(arg1)); 102 argMatchers.add(wrapMatcher(arg1));
90 if (arg2 == _noArg) return; 103 if (arg2 == _noArg) return;
91 argMatchers.add(wrapMatcher(arg2)); 104 argMatchers.add(wrapMatcher(arg2));
92 if (arg3 == _noArg) return; 105 if (arg3 == _noArg) return;
93 argMatchers.add(wrapMatcher(arg3)); 106 argMatchers.add(wrapMatcher(arg3));
94 if (arg4 == _noArg) return; 107 if (arg4 == _noArg) return;
(...skipping 21 matching lines...) Expand all
116 d.add(name).add('('); 129 d.add(name).add('(');
117 for (var i = 0; i < argMatchers.length; i++) { 130 for (var i = 0; i < argMatchers.length; i++) {
118 if (i > 0) d.add(', '); 131 if (i > 0) d.add(', ');
119 d.addDescriptionOf(argMatchers[i]); 132 d.addDescriptionOf(argMatchers[i]);
120 } 133 }
121 d.add(')'); 134 d.add(')');
122 return d.toString(); 135 return d.toString();
123 } 136 }
124 137
125 /** 138 /**
126 * Given a [method] name oand list of [arguments], return true 139 * Given a [method] name and list of [arguments], return true
127 * if it matches this [CallMatcher. 140 * if it matches this [CallMatcher.
128 */ 141 */
129 bool matches(String method, List arguments) { 142 bool matches(String method, List arguments) {
130 if (method != this.name) { 143 if (method != this.name) {
131 return false; 144 return false;
132 } 145 }
133 if (arguments.length < argMatchers.length) { 146 if (arguments.length < argMatchers.length) {
134 throw new Exception("Less arguments than matchers for $name"); 147 throw new Exception("Less arguments than matchers for $name");
135 } 148 }
136 for (var i = 0; i < argMatchers.length; i++) { 149 for (var i = 0; i < argMatchers.length; i++) {
137 if (!argMatchers[i].matches(arguments[i])) { 150 if (!argMatchers[i].matches(arguments[i])) {
138 return false; 151 return false;
139 } 152 }
140 } 153 }
141 return true; 154 return true;
142 } 155 }
143 } 156 }
144 157
145 /** [callsTo] returns a CallMatcher for the specified signature. */ 158 /** [callsTo] returns a CallMatcher for the specified signature. */
146 CallMatcher callsTo(String method, [ arg0 = _noArg, 159 CallMatcher callsTo(String method, [
160 arg0 = _noArg,
147 arg1 = _noArg, 161 arg1 = _noArg,
148 arg2 = _noArg, 162 arg2 = _noArg,
149 arg3 = _noArg, 163 arg3 = _noArg,
150 arg4 = _noArg, 164 arg4 = _noArg,
151 arg5 = _noArg, 165 arg5 = _noArg,
152 arg6 = _noArg, 166 arg6 = _noArg,
153 arg7 = _noArg, 167 arg7 = _noArg,
154 arg8 = _noArg, 168 arg8 = _noArg,
155 arg9 = _noArg]) { 169 arg9 = _noArg]) {
156 return new CallMatcher(method, arg0, arg1, arg2, arg3, arg4, 170 return new CallMatcher(method, arg0, arg1, arg2, arg3, arg4,
(...skipping 10 matching lines...) Expand all
167 181
168 Behavior (this.matcher) { 182 Behavior (this.matcher) {
169 actions = new List<Responder>(); 183 actions = new List<Responder>();
170 } 184 }
171 185
172 /** 186 /**
173 * Adds a [Responder] that returns a [value] for [count] calls 187 * Adds a [Responder] that returns a [value] for [count] calls
174 * (1 by default). 188 * (1 by default).
175 */ 189 */
176 Behavior thenReturn(value, [count = 1]) { 190 Behavior thenReturn(value, [count = 1]) {
177 actions.add(new Responder(value, count, RETURN)); 191 actions.add(new Responder(value, count, _Action.RETURN));
178 return this; // For chaining calls. 192 return this; // For chaining calls.
179 } 193 }
180 194
181 /** Adds a [Responder] that repeatedly returns a [value]. */ 195 /** Adds a [Responder] that repeatedly returns a [value]. */
182 Behavior alwaysReturn(value) { 196 Behavior alwaysReturn(value) {
183 return thenReturn(value, 0); 197 return thenReturn(value, 0);
184 } 198 }
185 199
186 /** 200 /**
187 * Adds a [Responder] that throws [value] [count] 201 * Adds a [Responder] that throws [value] [count]
188 * times (1 by default). 202 * times (1 by default).
189 */ 203 */
190 Behavior thenThrow(value, [count = 1]) { 204 Behavior thenThrow(value, [count = 1]) {
191 actions.add(new Responder(value, count, THROW)); 205 actions.add(new Responder(value, count, _Action.THROW));
192 return this; // For chaining calls. 206 return this; // For chaining calls.
193 } 207 }
194 208
195 /** Adds a [Responder] that throws [value] endlessly. */ 209 /** Adds a [Responder] that throws [value] endlessly. */
196 Behavior alwaysThrow(value) { 210 Behavior alwaysThrow(value) {
197 return thenThrow(value, 0); 211 return thenThrow(value, 0);
198 } 212 }
199 213
200 /** 214 /**
201 * [thenCall] creates a proxy Responder, that is called [count] 215 * [thenCall] creates a proxy Responder, that is called [count]
202 * times (1 by default; 0 is used for unlimited calls, and is 216 * times (1 by default; 0 is used for unlimited calls, and is
203 * exposed as [alwaysCall]). [value] is the function that will 217 * exposed as [alwaysCall]). [value] is the function that will
204 * be called with the same arguments that were passed to the 218 * be called with the same arguments that were passed to the
205 * mock. Proxies can be used to wrap real objects or to define 219 * mock. Proxies can be used to wrap real objects or to define
206 * more complex return/throw behavior. You could even (if you 220 * more complex return/throw behavior. You could even (if you
207 * wanted) use proxies to emulate the behavior of thenReturn; 221 * wanted) use proxies to emulate the behavior of thenReturn;
208 * e.g.: 222 * e.g.:
209 * 223 *
210 * m.when(callsTo('foo')).thenReturn(0) 224 * m.when(callsTo('foo')).thenReturn(0)
211 * 225 *
212 * is equivalent to: 226 * is equivalent to:
213 * 227 *
214 * m.when(callsTo('foo')).thenCall(() => 0) 228 * m.when(callsTo('foo')).thenCall(() => 0)
215 */ 229 */
216 Behavior thenCall(value, [count = 1]) { 230 Behavior thenCall(value, [count = 1]) {
217 actions.add(new Responder(value, count, PROXY)); 231 actions.add(new Responder(value, count, _Action.PROXY));
218 return this; // For chaining calls. 232 return this; // For chaining calls.
219 } 233 }
220 234
221 /** Creates a repeating proxy call. */ 235 /** Creates a repeating proxy call. */
222 Behavior alwaysCall(value) { 236 Behavior alwaysCall(value) {
223 return thenCall(value, 0); 237 return thenCall(value, 0);
224 } 238 }
225 239
226 /** Returns true if a method call matches the [Behavior]. */ 240 /** Returns true if a method call matches the [Behavior]. */
227 bool matches(name, args) => matcher.matches(name, args); 241 bool matches(String method, List args) => matcher.matches(method, args);
228 242
229 /** Returns the [matcher]'s representation. */ 243 /** Returns the [matcher]'s representation. */
230 String toString() => matcher.toString(); 244 String toString() => matcher.toString();
231 } 245 }
232 246
233 /** 247 /**
234 * Every call to a [Mock] object method is logged. The logs are 248 * Every call to a [Mock] object method is logged. The logs are
235 * kept in instances of [LogEntry]. 249 * kept in instances of [LogEntry].
236 */ 250 */
237 class LogEntry { 251 class LogEntry {
238 final String name; // The method name. 252 /** The time of the event. */
239 final List args; // The parameters. 253 Date when;
Siggi Cherem (dart-lang) 2012/07/09 22:11:05 when => time? (otherwise it is confusing because o
gram 2012/07/09 22:35:29 Done.
240 final int action; // The behavior that resulted.
241 final value; // The value that was returned (if no throw).
242 254
243 const LogEntry(this.name, this.args, this.action, [this.value = null]); 255 /** The mock object name, if any. */
256 final String mockName;
257
258 /** The method name. */
259 final String methodName;
260
261 /** The parameters. */
262 final List args;
263
264 /** The behavior that resulted. */
265 final _Action action;
266
267 /** The value that was returned (if no throw). */
268 final value;
269
270 LogEntry(this.mockName, this.methodName,
271 this.args, this.action, [this.value]) {
272 when = new Date.now();
273 }
274
275 String _pad2(int val) => (val >= 10 ? '$val' : '0$val');
276
277 String toString([Date baseTime]) {
278 Description d = new StringDescription();
279 if (baseTime == null) {
280 // Show absolute time.
281 d.add('${when.hour}:${_pad2(when.minute)}:'
282 '${_pad2(when.second)}.${when.millisecond}> ');
283 } else {
284 // Show relative time.
285 int delta = when.millisecondsSinceEpoch - baseTime.millisecondsSinceEpoch;
286 int secs = delta ~/ 1000;
287 int msecs = delta % 1000;
288 d.add('$secs.$msecs> ');
289 }
290 d.add('${_qualifiedName(mockName, methodName)}(');
291 for (var i = 0; i < args.length; i++) {
292 if (i != 0) d.add(', ');
293 d.addDescriptionOf(args[i]);
294 }
295 d.add(') ${action == _Action.THROW ? "threw" : "returned"} ');
296 d.addDescriptionOf(value);
297 return d.toString();
298 }
299 }
300
301 /** Utility function for optionally qualified method names */
302 String _qualifiedName(String owner, String method) {
303 if (owner == null) {
304 return method;
305 } else {
306 return '$owner.$method';
307 }
244 } 308 }
245 309
246 /** 310 /**
247 * We do verification on a list of [LogEntry]s. To allow chaining 311 * We do verification on a list of [LogEntry]s. To allow chaining
248 * of calls to verify, we encapsulate such a list in the [LogEntryList] 312 * of calls to verify, we encapsulate such a list in the [LogEntryList]
249 * class. 313 * class.
250 */ 314 */
251 class LogEntryList { 315 class LogEntryList {
252 final String filter; 316 final String filter;
253 final List<LogEntry> logs; 317 List<LogEntry> logs;
254 const LogEntryList(this.logs, [this.filter = null]); 318 LogEntryList([this.filter]) {
319 logs = new List<LogEntry>();
320 }
255 321
256 /** Add a [LogEntry] to the log. */ 322 /** Add a [LogEntry] to the log. */
257 add(LogEntry entry) => logs.add(entry); 323 add(LogEntry entry) => logs.add(entry);
258 324
259 /** 325 /**
260 * Create a new [LogEntryList] consisting of [LogEntry]s from 326 * Create a new [LogEntryList] consisting of [LogEntry]s from
261 * this list that match the specified [logfilter]. If [destructive] 327 * this list that match the specified [mockName] and [logFilter].
328 * If [mockName] is null, all entries will be checked. If [destructive]
262 * is true, the log entries are removed from the original list. 329 * is true, the log entries are removed from the original list.
263 */ 330 */
264 LogEntryList getMatches(CallMatcher logfilter, bool destructive) { 331 LogEntryList getMatches(String mockName,
265 LogEntryList rtn = 332 CallMatcher logFilter,
266 new LogEntryList(new List<LogEntry>(), logfilter.toString()); 333 [Matcher actionMatcher,
334 bool destructive = false]) {
335 String filterName = _qualifiedName(mockName, logFilter.toString());
336 LogEntryList rtn = new LogEntryList(filterName);
267 for (var i = 0; i < logs.length; i++) { 337 for (var i = 0; i < logs.length; i++) {
268 LogEntry entry = logs[i]; 338 LogEntry entry = logs[i];
269 if (logfilter.matches(entry.name, entry.args)) { 339 if (mockName != null && mockName != entry.mockName) {
270 rtn.add(entry); 340 continue;
271 if (destructive) { 341 }
272 logs.removeRange(i--, 1); 342 if (logFilter.matches(entry.methodName, entry.args)) {
343 if (actionMatcher == null || actionMatcher.matches(entry)) {
344 rtn.add(entry);
345 if (destructive) {
346 logs.removeRange(i--, 1);
347 }
273 } 348 }
274 } 349 }
275 } 350 }
276 return rtn; 351 return rtn;
277 } 352 }
278 353
279 /** Apply a unit test [Matcher] to the [LogEntryList]. */ 354 /** Apply a unit test [Matcher] to the [LogEntryList]. */
280 LogEntryList verify(Matcher matcher) { 355 LogEntryList verify(Matcher matcher) {
281 if (_mockFailureHandler == null) { 356 if (_mockFailureHandler == null) {
282 _mockFailureHandler = 357 _mockFailureHandler =
283 new _MockFailureHandler(getOrCreateExpectFailureHandler()); 358 new _MockFailureHandler(getOrCreateExpectFailureHandler());
284 } 359 }
285 expect(logs, matcher, filter, _mockFailureHandler); 360 expect(logs, matcher, filter, _mockFailureHandler);
286 return this; 361 return this;
287 } 362 }
363
364 String toString([Date baseTime]) {
365 String s = '';
366 for (var e in logs) {
367 s = '$s${e.toString(baseTime)}\n';
368 }
369 return s;
370 }
288 } 371 }
289 372
290 /** 373 /**
291 * [_TimesMatcher]s are used to make assertions about the number of 374 * [_TimesMatcher]s are used to make assertions about the number of
292 * times a method was called. 375 * times a method was called.
293 */ 376 */
294 class _TimesMatcher extends BaseMatcher { 377 class _TimesMatcher extends BaseMatcher {
295 final int min, max; 378 final int min, max;
296 379
297 const _TimesMatcher(this.min, [this.max = -1]); 380 const _TimesMatcher(this.min, [this.max = -1]);
(...skipping 11 matching lines...) Expand all
309 } else { 392 } else {
310 description.add('between $min and $max'); 393 description.add('between $min and $max');
311 } 394 }
312 return description.add(' times'); 395 return description.add(' times');
313 } 396 }
314 397
315 Description describeMismatch(log, Description mismatchDescription) => 398 Description describeMismatch(log, Description mismatchDescription) =>
316 mismatchDescription.add('was called ${log.length} times'); 399 mismatchDescription.add('was called ${log.length} times');
317 } 400 }
318 401
319 /** [calledExactly] matches an exact number of calls. */ 402 /** [happenedExactly] matches an exact number of calls. */
320 Matcher calledExactly(count) { 403 Matcher happenedExactly(count) {
321 return new _TimesMatcher(count, count); 404 return new _TimesMatcher(count, count);
322 } 405 }
323 406
324 /** [calledAtLeast] matches a minimum number of calls. */ 407 /** [happenedAtLeast] matches a minimum number of calls. */
325 Matcher calledAtLeast(count) { 408 Matcher happenedAtLeast(count) {
326 return new _TimesMatcher(count); 409 return new _TimesMatcher(count);
327 } 410 }
328 411
329 /** [calledAtMost] matches a maximum number of calls. */ 412 /** [happenedAtMost] matches a maximum number of calls. */
330 Matcher calledAtMost(count) { 413 Matcher happenedAtMost(count) {
331 return new _TimesMatcher(0, count); 414 return new _TimesMatcher(0, count);
332 } 415 }
333 416
334 /** [neverCalled] matches zero calls. */ 417 /** [neverHappened] matches zero calls. */
335 final Matcher neverCalled = const _TimesMatcher(0, 0); 418 final Matcher neverHappened = const _TimesMatcher(0, 0);
336 419
337 /** [calledOnce] matches exactly one call. */ 420 /** [happenedOnce] matches exactly one call. */
338 final Matcher calledOnce = const _TimesMatcher(1, 1); 421 final Matcher happenedOnce = const _TimesMatcher(1, 1);
339 422
340 /** [calledAtLeastOnce] matches one or more calls. */ 423 /** [happenedAtLeastOnce] matches one or more calls. */
341 final Matcher calledAtLeastOnce = const _TimesMatcher(1); 424 final Matcher happenedAtLeastOnce = const _TimesMatcher(1);
342 425
343 /** [calledAtMostOnce] matches zero or one call. */ 426 /** [happenedAtMostOnce] matches zero or one call. */
344 final Matcher calledAtMostOnce = const _TimesMatcher(0, 1); 427 final Matcher happenedAtMostOnce = const _TimesMatcher(0, 1);
345 428
346 /** Special values for use with [_ResultMatcher] [frequency]. */
347 final int ALL = 0;
348 final int SOME = 1;
349 final int NONE = 2;
350 /** 429 /**
351 * [_ResultMatcher]s are used to make assertions about the results 430 * [_ResultMatcher]s are used to make assertions about the results
352 * of method calls. When filtering an execution log by calling 431 * of method calls. These can be used as optional parameters to [getLogs].
353 * [forThe], a [LogEntrySet] of matching call logs is returned;
354 * [_ResultMatcher]s can then assert various things about this
355 * (sub)set of logs.
356 */ 432 */
357 class _ResultMatcher extends BaseMatcher { 433 class _ResultMatcher extends BaseMatcher {
358 final int action; 434 final _Action action;
359 final value; 435 final Matcher value;
360 final int frequency; // -1 for all, 0 for none, 1 for some.
361 436
362 const _ResultMatcher(this.action, this.value, this.frequency); 437 const _ResultMatcher(this.action, this.value);
438
439 bool matches(item) {
440 if (item is! LogEntry) {
441 return false;
442 }
443 // normalize the action; _PROXY is like _RETURN.
444 _Action eaction = item.action;
445 if (eaction == _Action.PROXY) {
446 eaction = _Action.RETURN;
447 }
448 return (eaction == action && value.matches(item.value));
449 }
450
451 Description describe(Description description) {
452 description.add(' to ');
453 if (action == _Action.RETURN || action == _Action.PROXY)
454 description.add('return ');
455 else
456 description.add('throw ');
457 return description.addDescriptionOf(value);
458 }
459
460 Description describeMismatch(item, Description mismatchDescription) {
461 if (item.action == _Action.RETURN || item.action == _Action.PROXY) {
462 mismatchDescription.add('returned ');
463 } else {
464 mismatchDescription.add('threw ');
465 }
466 mismatchDescription.add(item.value);
467 return mismatchDescription;
468 }
469 }
470
471 /**
472 *[returning] matches log entries where the call to a method returned
473 * a value that matched [value].
474 */
475 Matcher returning(value) =>
476 new _ResultMatcher(_Action.RETURN, wrapMatcher(value));
477
478 /**
479 *[throwing] matches log entrues where the call to a method threw
480 * a value that matched [value].
481 */
482 Matcher throwing(value) =>
483 new _ResultMatcher(_Action.THROW, wrapMatcher(value));
484
485 /** Special values for use with [_ResultSetMatcher] [frequency]. */
486 class _Frequency {
487 /** Every call/throw must match */
488 static final ALL = const _Frequency._('ALL');
489
490 /** At least one call/throw must match. */
491 static final SOME = const _Frequency._('SOME');
492
493 /** No calls/throws should match. */
494 static final NONE = const _Frequency._('NONE');
495
496 const _Frequency._(this.name);
497
498 final String name;
499 }
500
501 /**
502 * [_ResultSetMatcher]s are used to make assertions about the results
503 * of method calls. When filtering an execution log by calling
504 * [getLogs], a [LogEntrySet] of matching call logs is returned;
505 * [_ResultSetMatcher]s can then assert various things about this
506 * (sub)set of logs.
507 *
508 * We could make this class use _ResultMatcher but it doesn't buy that
509 * match and adds some perf hit, so there is some duplication here.
510 */
511 class _ResultSetMatcher extends BaseMatcher {
512 final _Action action;
513 final Matcher value;
514 final _Frequency frequency; // ALL, SOME, or NONE.
515
516 const _ResultSetMatcher(this.action, this.value, this.frequency);
363 517
364 bool matches(log) { 518 bool matches(log) {
365 for (LogEntry entry in log) { 519 for (LogEntry entry in log) {
366 // normalize the action; PROXY is like RETURN. 520 // normalize the action; _PROXY is like _RETURN.
367 int eaction = (entry.action == THROW) ? THROW : RETURN; 521 _Action eaction = entry.action;
522 if (eaction == _Action.PROXY) {
523 eaction = _Action.RETURN;
524 }
368 if (eaction == action && value.matches(entry.value)) { 525 if (eaction == action && value.matches(entry.value)) {
369 if (frequency == NONE) { 526 if (frequency == _Frequency.NONE) {
370 return false; 527 return false;
371 } else if (frequency == SOME) { 528 } else if (frequency == _Frequency.SOME) {
372 return true; 529 return true;
373 } 530 }
374 } else { 531 } else {
375 // Mismatch. 532 // Mismatch.
376 if (frequency == ALL) { // We need just one mismatch to fail. 533 if (frequency == _Frequency.ALL) { // We need just one mismatch to fail.
377 return false; 534 return false;
378 } 535 }
379 } 536 }
380 } 537 }
381 // If we get here, then if count is ALL we got all matches and 538 // If we get here, then if count is _ALL we got all matches and
382 // this is success; otherwise we got all mismatched which is 539 // this is success; otherwise we got all mismatched which is
383 // success for count == NONE and failure for count == SOME. 540 // success for count == _NONE and failure for count == _SOME.
384 return (frequency != SOME); 541 return (frequency != _Frequency.SOME);
385 } 542 }
386 543
387 Description describe(Description description) { 544 Description describe(Description description) {
388 description.add(' to '); 545 description.add(' to ');
389 description.add(frequency == ALL ? 'alway ' : 546 description.add(frequency == _Frequency.ALL ? 'alway ' :
390 (frequency == NONE ? 'never ' : 'sometimes ')); 547 (frequency == _Frequency.NONE ? 'never ' : 'sometimes '));
391 if (action == RETURN || action == PROXY) 548 if (action == _Action.RETURN || action == __Action.PROXY)
392 description.add('return '); 549 description.add('return ');
393 else 550 else
394 description.add('throw '); 551 description.add('throw ');
395 return description.addDescriptionOf(value); 552 return description.addDescriptionOf(value);
396 } 553 }
397 554
398 Description describeMismatch(log, Description mismatchDescription) { 555 Description describeMismatch(log, Description mismatchDescription) {
399 if (frequency != SOME) { 556 if (frequency != _Frequency.SOME) {
400 for (LogEntry entry in log) { 557 for (LogEntry entry in log) {
401 if (entry.action != action || !value.matches(entry.value)) { 558 if (entry.action != action || !value.matches(entry.value)) {
402 if (entry.action == RETURN || entry.action == PROXY) 559 if (entry.action == _Action.RETURN || entry.action == _Action.PROXY)
403 mismatchDescription.add('returned '); 560 mismatchDescription.add('returned ');
404 else 561 else
405 mismatchDescription.add('threw '); 562 mismatchDescription.add('threw ');
406 mismatchDescription.add(entry.value); 563 mismatchDescription.add(entry.value);
407 mismatchDescription.add(' at least once'); 564 mismatchDescription.add(' at least once');
408 break; 565 break;
409 } 566 }
410 } 567 }
411 } else { 568 } else {
412 mismatchDescription.add('never did'); 569 mismatchDescription.add('never did');
413 } 570 }
414 return mismatchDescription; 571 return mismatchDescription;
415 } 572 }
416 } 573 }
417 574
418 /** 575 /**
419 *[alwaysReturned] asserts that all matching calls to a method returned 576 *[alwaysReturned] asserts that all matching calls to a method returned
420 * a value that matched [value]. 577 * a value that matched [value].
421 */ 578 */
422 Matcher alwaysReturned(value) => 579 Matcher alwaysReturned(value) =>
423 new _ResultMatcher(RETURN, wrapMatcher(value), ALL); 580 new _ResultSetMatcher(_Action.RETURN, wrapMatcher(value), _Frequency.ALL);
424 581
425 /** 582 /**
426 *[sometimeReturned] asserts that at least one matching call to a method 583 *[sometimeReturned] asserts that at least one matching call to a method
427 * returned a value that matched [value]. 584 * returned a value that matched [value].
428 */ 585 */
429 Matcher sometimeReturned(value) => 586 Matcher sometimeReturned(value) =>
430 new _ResultMatcher(RETURN, wrapMatcher(value), SOME); 587 new _ResultSetMatcher(_Action.RETURN, wrapMatcher(value), _Frequency.SOME);
431 588
432 /** 589 /**
433 *[neverReturned] asserts that no matching calls to a method returned 590 *[neverReturned] asserts that no matching calls to a method returned
434 * a value that matched [value]. 591 * a value that matched [value].
435 */ 592 */
436 Matcher neverReturned(value) => 593 Matcher neverReturned(value) =>
437 new _ResultMatcher(RETURN, wrapMatcher(value), NONE); 594 new _ResultSetMatcher(_Action.RETURN, wrapMatcher(value), _Frequency.NONE);
438 595
439 /** 596 /**
440 *[alwaysThrew] asserts that all matching calls to a method threw 597 *[alwaysThrew] asserts that all matching calls to a method threw
441 * a value that matched [value]. 598 * a value that matched [value].
442 */ 599 */
443 Matcher alwaysThrew(value) => 600 Matcher alwaysThrew(value) =>
444 new _ResultMatcher(THROW, wrapMatcher(value), ALL); 601 new _ResultSetMatcher(_Action.THROW, wrapMatcher(value), _Frequency.ALL);
445 602
446 /** 603 /**
447 *[sometimeThrew] asserts that at least one matching call to a method threw 604 *[sometimeThrew] asserts that at least one matching call to a method threw
448 * a value that matched [value]. 605 * a value that matched [value].
449 */ 606 */
450 Matcher sometimeThrew(value) => 607 Matcher sometimeThrew(value) =>
451 new _ResultMatcher(THROW, wrapMatcher(value), SOME); 608 new _ResultSetMatcher(_Action.THROW, wrapMatcher(value), _Frequency.SOME);
452 609
453 /** 610 /**
454 *[neverThrew] asserts that no matching call to a method threw 611 *[neverThrew] asserts that no matching call to a method threw
455 * a value that matched [value]. 612 * a value that matched [value].
456 */ 613 */
457 Matcher neverThrew(value) => 614 Matcher neverThrew(value) =>
458 new _ResultMatcher(THROW, wrapMatcher(value), NONE); 615 new _ResultSetMatcher(_Action.THROW, wrapMatcher(value), _Frequency.NONE);
616
617 /** The shared log used for named mocks. */
618 LogEntryList sharedLog = null;
459 619
460 /** 620 /**
461 * [Mock] is the base class for all mocked objects, with 621 * [Mock] is the base class for all mocked objects, with
462 * support for basic mocking. 622 * support for basic mocking.
463 * 623 *
464 * To create a mock objects for some class T, create a new class using: 624 * To create a mock objects for some class T, create a new class using:
465 * 625 *
466 * class MockT extends Mock implements T {}; 626 * class MockT extends Mock implements T {};
467 * 627 *
468 * Then specify the behavior of the Mock for different methods using 628 * Then specify the behavior of the Mock for different methods using
469 * [when] (to select the method and parameters) and [thenReturn], 629 * [when] (to select the method and parameters) and [thenReturn],
470 * [alwaysReturn], [thenThrow], [alwaysThrow], [thenCall] or [alwaysCall]. 630 * [alwaysReturn], [thenThrow], [alwaysThrow], [thenCall] or [alwaysCall].
471 * [thenReturn], [thenThrow] and [thenCall] are one-shot so you would 631 * [thenReturn], [thenThrow] and [thenCall] are one-shot so you would
472 * typically call these more than once to specify a sequence of actions; 632 * typically call these more than once to specify a sequence of actions;
473 * this can be done with chained calls, e.g.: 633 * this can be done with chained calls, e.g.:
474 * 634 *
475 * m.when(callsTo('foo')). 635 * m.when(callsTo('foo')).
476 * thenReturn(0).thenReturn(1).thenReturn(2); 636 * thenReturn(0).thenReturn(1).thenReturn(2);
477 * 637 *
478 * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining 638 * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining
479 * to some other implementation. This provides a way to implement 'spies'. 639 * to some other implementation. This provides a way to implement 'spies'.
480 * 640 *
481 * You can then use the mock object. Once you are done, to verify the 641 * You can then use the mock object. Once you are done, to verify the
482 * behavior, use [forThe] to extract a relevant subset of method call 642 * behavior, use [getLogs] to extract a relevant subset of method call
483 * logs and apply [Matchers] to these through calling [verify]. 643 * logs and apply [Matchers] to these through calling [verify].
484 * 644 *
645 * A Mock can be given a name when constructed. In this case instead of
646 * keeping its own log, it uses a shared log. This can be useful to get an
647 * audit trail of interleaved behavior. It is the responsibility of the user
648 * to ensure that mock names, if used, are unique.
649 *
485 * Limitations: 650 * Limitations:
486 * - only positional parameters are supported (up to 10); 651 * - only positional parameters are supported (up to 10);
487 * - to mock getters you will need to include parentheses in the call 652 * - to mock getters you will need to include parentheses in the call
488 * (e.g. m.length() will work but not m.length). 653 * (e.g. m.length() will work but not m.length).
489 * 654 *
490 * Here is a simple example: 655 * Here is a simple example:
491 * 656 *
492 * class MockList extends Mock implements List {}; 657 * class MockList extends Mock implements List {};
493 * 658 *
494 * List m = new MockList(); 659 * List m = new MockList();
495 * m.when(callsTo('add', anything)).alwaysReturn(0); 660 * m.when(callsTo('add', anything)).alwaysReturn(0);
496 * 661 *
497 * m.add('foo'); 662 * m.add('foo');
498 * m.add('bar'); 663 * m.add('bar');
499 * 664 *
500 * getLogs(m, callsTo('add', anything)).verify(calledExactly(2)); 665 * getLogs(m, callsTo('add', anything)).verify(happenedExactly(2));
501 * getLogs(m, callsTo('add', 'foo')).verify(calledOnce); 666 * getLogs(m, callsTo('add', 'foo')).verify(happenedOnce);
502 * getLogs(m, callsTo('add', 'isNull)).verify(neverCalled); 667 * getLogs(m, callsTo('add', 'isNull)).verify(neverHappened);
503 * 668 *
504 * Note that we don't need to provide argument matchers for all arguments, 669 * Note that we don't need to provide argument matchers for all arguments,
505 * but we do need to provide arguments for all matchers. So this is allowed: 670 * but we do need to provide arguments for all matchers. So this is allowed:
506 * 671 *
507 * m.when(callsTo('add')).alwaysReturn(0); 672 * m.when(callsTo('add')).alwaysReturn(0);
508 * m.add(1, 2); 673 * m.add(1, 2);
509 * 674 *
510 * But this is not allowed and will throw an exception: 675 * But this is not allowed and will throw an exception:
511 * 676 *
512 * m.when(callsTo('add', anything, anything)).alwaysReturn(0); 677 * m.when(callsTo('add', anything, anything)).alwaysReturn(0);
(...skipping 10 matching lines...) Expand all
523 * class MockFoo extends Mock implements Foo { 688 * class MockFoo extends Mock implements Foo {
524 * Foo real; 689 * Foo real;
525 * MockFoo() { 690 * MockFoo() {
526 * real = new Foo(); 691 * real = new Foo();
527 * this.when(callsTo('bar')).alwaysCall(real.bar); 692 * this.when(callsTo('bar')).alwaysCall(real.bar);
528 * } 693 * }
529 * } 694 * }
530 * 695 *
531 */ 696 */
532 class Mock { 697 class Mock {
533 Map<String,Behavior> behaviors; /** The set of [behavior]s supported. */ 698 /** The mock name. Needed if the log is shared; optional otherwise. */
534 LogEntryList log; /** The [log] of calls made. */ 699 final String name;
535 700
536 Mock() { 701 /** The set of [behavior]s supported. */
702 Map<String,Behavior> behaviors;
703
704 /** The [log] of calls made. Only used if [name] is null. */
705 LogEntryList log;
706
707 /** How to handle unknown method calls - swallow or throw. */
708 final bool throwIfNoBehavior = false;
709
710 /*
711 * Default constructor. Unknown method calls are allowed and logged,
712 * the mock has no name, and has its own log.
713 */
714 Mock() : throwIfNoBehavior = false, name = null {
715 log = new LogEntryList();
537 behaviors = new Map<String,Behavior>(); 716 behaviors = new Map<String,Behavior>();
538 log = new LogEntryList(new List<LogEntry>()); 717 }
718
719 /**
720 * This constructor makes a mock that has a [name] and possibly uses
721 * a shared [log]. If [throwIfNoBehavior] is true, any calls to methods
722 * that have no defined behaviors will throw an exception; otherwise they
723 * will be allowed and logged (but will not do anything).
724 */
725 Mock.custom([this.name,
Siggi Cherem (dart-lang) 2012/07/09 22:11:05 please add tests for allocating Mock with a name a
gram 2012/07/09 22:35:29 Done.
726 this.log,
727 this.throwIfNoBehavior = false]) {
728 if (log == null) {
729 log = new LogEntryList();
730 }
731 behaviors = new Map<String,Behavior>();
539 } 732 }
540 733
541 /** 734 /**
542 * [when] is used to create a new or extend an existing [Behavior]. 735 * [when] is used to create a new or extend an existing [Behavior].
543 * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for 736 * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for
544 * that signature are returned (being created first if needed). 737 * that signature are returned (being created first if needed).
545 * 738 *
546 * Typical use case: 739 * Typical use case:
547 * 740 *
548 * mock.when(callsTo(...)).alwaysReturn(...); 741 * mock.when(callsTo(...)).alwaysReturn(...);
549 */ 742 */
550 Behavior when(CallMatcher logFilter) { 743 Behavior when(CallMatcher logFilter) {
551 String key = logFilter.toString(); 744 String key = logFilter.toString();
552 if (!behaviors.containsKey(key)) { 745 if (!behaviors.containsKey(key)) {
553 Behavior b = new Behavior(logFilter); 746 Behavior b = new Behavior(logFilter);
554 behaviors[key] = b; 747 behaviors[key] = b;
555 return b; 748 return b;
556 } else { 749 } else {
557 return behaviors[key]; 750 return behaviors[key];
558 } 751 }
559 } 752 }
560 753
561 /** 754 /**
562 * This is the handler for method calls. We loo through the list 755 * This is the handler for method calls. We loo through the list
563 * of [Behavior]s, and find the first match that still has return 756 * of [Behavior]s, and find the first match that still has return
564 * values available, and then do the action specified by that 757 * values available, and then do the action specified by that
565 * return value. If we find no [Behavior] to apply an exception is 758 * return value. If we find no [Behavior] to apply an exception is
566 * thrown. 759 * thrown.
567 */ 760 */
568 noSuchMethod(String name, List args) { 761 noSuchMethod(String method, List args) {
762 if (method.startsWith('get:')) {
763 method = 'get ${method.substring(4)}';
764 }
765 bool matchedMethodName = false;
569 for (String k in behaviors.getKeys()) { 766 for (String k in behaviors.getKeys()) {
570 Behavior b = behaviors[k]; 767 Behavior b = behaviors[k];
571 if (b.matches(name, args)) { 768 if (b.matcher.name == method) {
769 matchedMethodName = true;
770 }
771 if (b.matches(method, args)) {
572 List actions = b.actions; 772 List actions = b.actions;
573 if (actions == null || actions.length == 0) { 773 if (actions == null || actions.length == 0) {
574 continue; // No return values left in this Behavior. 774 continue; // No return values left in this Behavior.
575 } 775 }
576 // Get the first response. 776 // Get the first response.
577 Responder response = actions[0]; 777 Responder response = actions[0];
578 // If it is exhausted, remove it from the list. 778 // If it is exhausted, remove it from the list.
579 // Note that for endlessly repeating values, we started the count at 779 // Note that for endlessly repeating values, we started the count at
580 // 0, so we get a potentially useful value here, which is the 780 // 0, so we get a potentially useful value here, which is the
581 // (negation of) the number of times we returned the value. 781 // (negation of) the number of times we returned the value.
582 if (--response.count == 0) { 782 if (--response.count == 0) {
583 actions.removeRange(0, 1); 783 actions.removeRange(0, 1);
584 if (actions.length == 0) {
585 // Remove the behavior. Note that in the future there
586 // may be some value in preserving the behaviors for
587 // auditing purposes (e.g. how many times was this behavior used?).
588 // If we do decide to keep them and perf is an issue instead of
589 // deleting we could move this to a separate list.
590 behaviors.remove(k);
591 }
592 } 784 }
593 // Do the response. 785 // Do the response.
594 var action = response.action; 786 _Action action = response.action;
595 var value = response.value; 787 var value = response.value;
596 switch (action) { 788 if (action == _Action.RETURN) {
597 case RETURN: 789 log.add(new LogEntry(name, method, args, action, value));
598 log.add(new LogEntry(name, args, action, value)); 790 return value;
599 return value; 791 } else if (action == _Action.THROW) {
600 case THROW: 792 log.add(new LogEntry(name, method, args, action, value));
601 log.add(new LogEntry(name, args, action, value)); 793 throw value;
602 throw value; 794 } else if (action == _Action.PROXY) {
603 case PROXY: 795 var rtn;
604 var rtn; 796 switch (args.length) {
605 switch (args.length) { 797 case 0:
606 case 0: 798 rtn = value();
607 rtn = value(); 799 break;
608 break; 800 case 1:
609 case 1: 801 rtn = value(args[0]);
610 rtn = value(args[0]); 802 break;
611 break; 803 case 2:
612 case 2: 804 rtn = value(args[0], args[1]);
613 rtn = value(args[0], args[1]); 805 break;
614 break; 806 case 3:
615 case 3: 807 rtn = value(args[0], args[1], args[2]);
616 rtn = value(args[0], args[1], args[2]); 808 break;
617 break; 809 case 4:
618 case 4: 810 rtn = value(args[0], args[1], args[2], args[3]);
619 rtn = value(args[0], args[1], args[2], args[3]); 811 break;
620 break; 812 case 5:
621 case 5: 813 rtn = value(args[0], args[1], args[2], args[3], args[4]);
622 rtn = value(args[0], args[1], args[2], args[3], args[4]); 814 break;
623 break; 815 case 6:
624 case 6: 816 rtn = value(args[0], args[1], args[2], args[3],
625 rtn = value(args[0], args[1], args[2], args[3], 817 args[4], args[5]);
626 args[4], args[5]); 818 break;
627 break; 819 case 7:
628 case 7: 820 rtn = value(args[0], args[1], args[2], args[3],
629 rtn = value(args[0], args[1], args[2], args[3], 821 args[4], args[5], args[6]);
630 args[4], args[5], args[6]); 822 break;
631 break; 823 case 8:
632 case 8: 824 rtn = value(args[0], args[1], args[2], args[3],
633 rtn = value(args[0], args[1], args[2], args[3], 825 args[4], args[5], args[6], args[7]);
634 args[4], args[5], args[6], args[7]); 826 break;
635 break; 827 case 9:
636 case 9: 828 rtn = value(args[0], args[1], args[2], args[3],
637 rtn = value(args[0], args[1], args[2], args[3], 829 args[4], args[5], args[6], args[7], args[8]);
638 args[4], args[5], args[6], args[7], args[8]); 830 break;
639 break; 831 case 9:
640 case 9: 832 rtn = value(args[0], args[1], args[2], args[3],
641 rtn = value(args[0], args[1], args[2], args[3], 833 args[4], args[5], args[6], args[7], args[8], args[9]);
642 args[4], args[5], args[6], args[7], args[8], args[9]); 834 break;
643 break; 835 default:
644 default: 836 throw new Exception(
645 throw new Exception( 837 "Cannot proxy calls with more than 10 parameters");
646 "Cannot proxy calls with more than 10 parameters"); 838 }
647 } 839 log.add(new LogEntry(name, method, args, action, rtn));
648 log.add(new LogEntry(name, args, action, rtn)); 840 return rtn;
649 return rtn;
650 } 841 }
651 } 842 }
652 } 843 }
653 throw new Exception('No behavior specified for method $name'); 844 if (matchedMethodName) {
845 // User did specify behavior for this method, but all the
846 // actions are exhausted. This is considered an error.
847 throw new Exception('No more actions for method '
848 '${_qualifiedName(name, method)}');
849 } else if (throwIfNoBehavior) {
850 throw new Exception('No behavior specified for method '
851 '${_qualifiedName(name, method)}');
852 }
853 // User hasn't specified behavior for this method; we don't throw
854 // so we can underspecify.
855 log.add(new LogEntry(name, method, args, _Action.IGNORE));
654 } 856 }
655 857
656 /** [verifyZeroInteractions] returns true if no calls were made */ 858 /** [verifyZeroInteractions] returns true if no calls were made */
657 bool verifyZeroInteractions() => log.logs.length == 0; 859 bool verifyZeroInteractions() => log.logs.length == 0;
860
861 /**
862 * [getLogs] extracts all calls from the call log that match the
863 * [logFilter] [CallMatcher], and returns the matching list of
864 * [LogEntry]s. If [destructive] is false (the default) the matching
865 * calls are left in the log, else they are removed. Removal allows
866 * us to verify a set of interactions and then verify that there are
867 * no other interactions left. [actionMatcher] can be used to further
868 * restrict the returned logs based on the action the mock performed.
869 *
870 * Typical usage:
871 *
872 * getLogs(callsTo(...)).verify(...);
873 */
874 LogEntryList getLogs(CallMatcher logFilter, [Matcher actionMatcher,
875 bool destructive = false]) {
876 return log.getMatches(name, logFilter, actionMatcher, destructive);
877 }
658 } 878 }
659
660 /**
661 * [getLogs] extracts all calls from the call log of [mock] that match the
662 * [logFilter] [CallMatcher], and returns the matching list of
663 * [LogEntry]s. If [destructive] is false (the default) the matching
664 * calls are left in the mock object's log, else they are removed.
665 * Removal allows us to verify a set of interactions and then verify
666 * that there are no other interactions left.
667 *
668 * Typical usage:
669 *
670 * getLogs(mock, callsTo(...)).verify(...);
671 */
672 LogEntryList getLogs(Mock mock, CallMatcher logFilter,
673 [bool destructive = false]) {
674 return mock.log.getMatches(logFilter, destructive);
675 }
676
677
OLDNEW
« no previous file with comments | « no previous file | lib/unittest/operator_matchers.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698