Chromium Code Reviews| OLD | NEW |
|---|---|
| (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 | |
| OLD | NEW |