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