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 | |
| 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 | |
| OLD | NEW |