Chromium Code Reviews| Index: lib/unittest/mock.dart |
| =================================================================== |
| --- lib/unittest/mock.dart (revision 0) |
| +++ lib/unittest/mock.dart (revision 0) |
| @@ -0,0 +1,435 @@ |
| +// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| +// for details. All rights reserved. Use of this source code is governed by a |
| +// BSD-style license that can be found in the LICENSE file. |
| + |
| +/** |
| + * Support for basic mocking. |
| + * |
| + * To create a mock objects for some class T, create a new class using: |
| + * |
| + * class MockT extends Mock implements T {}; |
| + * |
| + * Then specify the behavior of the Mock for different methods using |
| + * [when] (to select the method and parameters) and [thenReturn], |
| + * [alwaysReturn], [thenThrow] and/or [alwaysThrow]. |
| + * |
| + * You can then use the mock object. Once you are done, to verify the |
| + * behavior, use [verify] to extract a relevant subset of method call |
| + * logs and apply [Matchers] to these. |
| + * |
| + * Limitations: |
| + * - only positional parameters are supported (up to 10); |
| + * - to mock getters you will need to include parentheses. |
| + * |
| + * Here is a simple example: |
| + * |
| + * class MockList extends Mock implements List {}; |
| + * |
| + * List m = new MockList(); |
| + * m.when('add', anything).alwaysReturn(0); |
| + * |
| + * m.add('foo'); |
| + * m.add('bar'); |
| + * |
| + * m.verify('add', anything, was:calledExactly(2)); |
| + * m.verify('add', 'foo', was:calledOnce); |
| + * m.verify('add', 'isNull, was:neverCalled); |
| + */ |
| + |
|
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
|
| +/** |
| + * The error formatter for mocking is a bit different from the default one |
| + * for unit testing; instead of the third argument being a 'reason' |
| + * it is instead a [signature] describing the method signature filter |
| + * that was used to select the logs that were verified. |
| + */ |
| + |
|
Siggi Cherem (dart-lang)
2012/06/26 21:35:44
remove empty line
gram
2012/06/26 23:39:59
Done.
|
| +String _mockingErrorFormatter(actual, Matcher matcher, String signature) { |
| + var description = new StringDescription(); |
| + description.add('Expected ${signature} ').addDescriptionOf(matcher). |
| + add('\n but: '); |
| + matcher.describeMismatch(actual, description); |
| + return description.toString(); |
| +} |
| + |
| +/** |
| + * The failure handler for the [expect()] calls that occur in [verify()] |
| + * methods in the mock objects. This calls the real failure handler used |
| + * by the unit test library after formatting the error message with |
| + * the custom formatter. |
| + */ |
| +class _MockFailureHandler implements FailureHandler { |
| + FailureHandler proxy; |
| + _MockFailureHandler(this.proxy); |
| + void fail(String reason) { |
| + proxy.fail(reason); |
| + } |
| + void failMatch(actual, Matcher matcher, String reason) { |
| + proxy.fail(_mockingErrorFormatter(actual, matcher, reason)); |
| + } |
| +} |
| + |
| +_MockFailureHandler _mockFailureHandler = null; |
| + |
| +/** |
| + * [_NoArg] is a class with an instance used to fake varargs. |
| + */ |
| +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.
|
| + const _NoArg(); |
| + String toString() => ''; |
| +} |
| + |
| +/** |
| + * [noArg] is a sentinel value representing no argument. |
| + */ |
| +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.
|
| + |
| +/** |
| + * The behavior of a method call in the mock library is specified |
| + * with [BehaviorValue]s. A [BehaviorValue] has a [value] to throw |
| + * or return (depending on whether [isThrow] is true or not, respectively), |
| + * and can either be one-shot or repeating, depending on the value of |
| + * of [isOneShot]. |
| + */ |
| +class BehaviorValue { |
| + var value; |
| + bool isThrow; |
| + 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.
|
| + BehaviorValue(this.value, [this.isOneShot = true, this.isThrow = false]); |
| +} |
| + |
| +/** |
| + * A [CallMatcher] is a special matcher used to match method calls (i.e. |
| + * a method name and set of arguments). It is not a [Matcher] like the |
| + * unit test [Matcher], but instead represents a collection of [Matcher]s, |
| + * one per argument, that will be applied to the parameters to decide if |
| + * the method call is a match. |
| + */ |
| +class CallMatcher { |
| + String name; |
| + List<Matcher> argMatchers; |
| + |
| + CallMatcher(String method, [ |
| + arg0 = noArg, |
| + arg1 = noArg, |
| + arg2 = noArg, |
| + arg3 = noArg, |
| + arg4 = noArg, |
| + arg5 = noArg, |
| + arg6 = noArg, |
| + arg7 = noArg, |
| + arg8 = noArg, |
| + arg9 = noArg]) { |
| + name = method; |
| + argMatchers = new List<Matcher>(); |
| + if (arg0 != noArg) { |
| + argMatchers.add(wrapMatcher(arg0)); |
| + if (arg1 != noArg) { |
| + argMatchers.add(wrapMatcher(arg1)); |
| + if (arg2 != noArg) { |
| + argMatchers.add(wrapMatcher(arg2)); |
| + if (arg3 != noArg) { |
| + argMatchers.add(wrapMatcher(arg3)); |
| + if (arg4 != noArg) { |
| + argMatchers.add(wrapMatcher(arg4)); |
| + if (arg5 != noArg) { |
| + argMatchers.add(wrapMatcher(arg5)); |
| + if (arg6 != noArg) { |
| + argMatchers.add(wrapMatcher(arg6)); |
| + if (arg7 != noArg) { |
| + argMatchers.add(wrapMatcher(arg7)); |
| + if (arg8 != noArg) { |
| + argMatchers.add(wrapMatcher(arg8)); |
| + if (arg9 != noArg) { |
| + 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.
|
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + |
| + /** |
| + * We keep our behavior specifications in a Map, which is keyed |
| + * by the [CallMatcher]. To make the keys unique and to get a |
| + * descriptive value for the [CallMatcher] we have this override |
| + * of [toString()]. |
| + */ |
| + String toString() { |
| + Description d = new StringDescription(); |
| + d.add(name).add('('); |
| + for (var i = 0; i < argMatchers.length; i++) { |
| + if (i > 0) d.add(', '); |
| + d.addDescriptionOf(argMatchers[i]); |
| + } |
| + d.add(')'); |
| + return d.toString(); |
| + } |
| + |
| + /** |
| + * Given a [method] name oand list of [arguments], return true |
| + * if it matches this [CallMatcher. |
| + */ |
| + bool matches(String method, List arguments) { |
| + if (method != this.name || arguments.length != argMatchers.length) { |
| + return false; |
| + } |
| + for (var i = 0; i < arguments.length; i++) { |
| + if (!argMatchers[i].matches(arguments[i])) { |
| + return false; |
| + } |
| + } |
| + return true; |
| + } |
| +} |
| + |
| +/** |
| + * A [Behavior] represents how a [Mock] will respond to one particular |
| + * type of method call. |
| + */ |
| +class Behavior { |
| + CallMatcher matcher; // The method call matcher. |
| + List<BehaviorValue> returnValues; // The values to return/throw. |
| + |
| + Behavior (this.matcher) { |
| + returnValues = new List<BehaviorValue>(); |
| + } |
| + |
| + /** [thenReturn] creates a one-shot return value. */ |
| + 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.
|
| + returnValues.add(new BehaviorValue(value)); |
| + return this; // For chaining calls. |
| + } |
| + |
| + /** [alwaysReturn] creates a repeating return value. */ |
| + Behavior alwaysReturn(value) { |
| + returnValues.add(new BehaviorValue(value, false)); |
| + return this; // For chaining calls. |
| + } |
| + |
| + /** [thenThrow] creates a one-shot exception. */ |
| + Behavior thenThrow(value) { |
| + returnValues.add(new BehaviorValue(value, true, true)); |
| + return this; // For chaining calls. |
| + } |
| + |
| + /** [alwaysThrow] creates a repeating exception. */ |
| + Behavior alwaysThrow(value) { |
| + returnValues.add(new BehaviorValue(value, false, true)); |
| + return this; // For chaining calls. |
| + } |
| + |
| + /** [matches] return true if a method call matches the [Behavior]. */ |
| + bool matches(name, args) => matcher.matches(name, args); |
| +} |
| + |
| +/** |
| + * Every call to a [Mock] object method is logged. The logs are |
| + * kept in instances of [LogEntry]. |
| + */ |
| +class LogEntry { |
| + final String name; // The method name. |
| + final List args; // The parameters. |
| + final BehaviorValue result; // The behavior that resulted. |
| + |
| + const LogEntry(this.name, this.args, this.result); |
| +} |
| + |
| +/** |
| + * We do verification on a list of [LogEntry]s. To allow chaining |
| + * of calls to verify, we encapsulate such a list in the [LogEntryList] |
| + * class. |
| + */ |
| +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,
|
| + final String filter; |
| + final List<LogEntry> logs; |
| + const LogEntryList(this.logs, [this.filter = null]); |
| + |
| + /** Add a [LogEntry] to the log. */ |
| + void add(LogEntry entry) { |
| + logs.add(entry); |
| + } |
| + |
| + /** |
| + * Create a new [LogEntryList] consisting of [LogEntry]s from |
| + * this list that match the specified [logfilter]. |
| + */ |
| + LogEntryList getMatches(CallMatcher logfilter) { |
| + LogEntryList rtn = |
| + new LogEntryList(new List<LogEntry>(), logfilter.toString()); |
| + for (var i = 0; i < logs.length; i++) { |
| + LogEntry entry = logs[i]; |
| + if (logfilter.matches(entry.name, entry.args)) { |
| + rtn.add(entry); |
| + } |
| + } |
| + return rtn; |
| + } |
| + |
| + /** Apply a unit test [Matcher] to the [LogEntryList]. */ |
| + LogEntryList verify(Matcher matcher) { |
| + if (_mockFailureHandler == null) { |
| + _mockFailureHandler = new _MockFailureHandler(getFailureHandler()); |
| + } |
| + expect(logs, matcher, filter, _mockFailureHandler); |
| + return this; |
| + } |
| +} |
| + |
| +/** |
| + * [_TimesMatcher]s are used to make assertions about the number of |
| + * times a method was called. |
| + */ |
| +class _TimesMatcher extends BaseMatcher { |
| + final int min, max; |
| + const _TimesMatcher(this.min, [this.max = -1]); |
| + bool matches(log) => log.length >= min && (max < 0 || log.length <= max); |
| + Description describe(Description description) { |
| + description.add(' to be called '); |
| + if (max < 0) { |
| + description.add('at least $min'); |
| + } else if (max == min) { |
| + description.add('$max'); |
| + } else if (min == 0) { |
| + description.add('at most $max'); |
| + } else { |
| + description.add('between $min and $max'); |
| + } |
| + return description.add(' times'); |
| + } |
| + Description describeMismatch(log, Description mismatchDescription) => |
| + mismatchDescription.add('was called ${log.length} times'); |
| +} |
| + |
| +/** [calledExactly] matches an exact number of calls. */ |
| +Matcher calledExactly(count) { |
| + return new _TimesMatcher(count, count); |
| +} |
| + |
| +/** [calledAtLeast] matches a minimum number of calls. */ |
| +Matcher calledAtLeast(count) { |
| + return new _TimesMatcher(count); |
| +} |
| + |
| +/** [calledAtMost] matches a maximum number of calls. */ |
| +Matcher calledAtMost(count) { |
| + return new _TimesMatcher(0, count); |
| +} |
| + |
| +/** [neverCalled] matches zero calls. */ |
| +final Matcher neverCalled = const _TimesMatcher(0, 0); |
| + |
| +/** [calledOnce] matches exactly one call. */ |
| +final Matcher calledOnce = const _TimesMatcher(1, 1); |
| + |
| +/** [calledAtLeastOnce] matches one or more calls. */ |
| +final Matcher calledAtLeastOnce = const _TimesMatcher(1); |
| + |
| +/** [calledAtMostOnce] matches zero or one call. */ |
| +final Matcher calledAtMostOnce = const _TimesMatcher(0, 1); |
| + |
| +/** [Mock] is the base class for all mocked objects. */ |
| +class Mock { |
| + Map<String,Behavior> behaviors; /** The set of [behavior]s supported. */ |
| + LogEntryList log; /** The [log] of calls made. */ |
| + |
| + Mock() { |
| + behaviors = new Map<String,Behavior>(); |
| + log = new LogEntryList(new List<LogEntry>()); |
| + } |
| + |
| + /** |
| + * [when] is used to create a new or extend an existing [Behavior]. |
| + * The [method] name and the argument [Matcher] is specified. A |
| + * corresponding [CallMatcher] is created, and the [Behavior]s for |
| + * its signature are returned (being created first if needed). |
| + */ |
| + Behavior when(String method, [ |
| + arg0 = noArg, |
| + arg1 = noArg, |
| + arg2 = noArg, |
| + arg3 = noArg, |
| + arg4 = noArg, |
| + arg5 = noArg, |
| + arg6 = noArg, |
| + arg7 = noArg, |
| + arg8 = noArg, |
| + arg9 = noArg]) { |
| + CallMatcher logfilter = new CallMatcher(method, |
| + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); |
| + String key = logfilter.toString(); |
| + if (!behaviors.containsKey(key)) { |
| + Behavior b = new Behavior(logfilter); |
| + behaviors[key] = b; |
| + return b; |
| + } else { |
| + return behaviors[key]; |
| + } |
| + } |
| + |
| + /** |
| + * This is the handler for method calls. We loo through the list |
| + * of [Behavior]s, and find the first match that still has return |
| + * values available, and then do the action specified by that |
| + * return value. If we find no [Behavior] to apply an exception is |
| + * thrown. |
| + */ |
| + noSuchMethod(String name, List args) { |
| + for (Behavior b in behaviors.getValues()) { |
| + if (b.matches(name, args)) { |
| + List rv = b.returnValues; |
| + if (rv == null || rv.length == 0) { |
| + 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
|
| + } |
| + // Get the first response. |
| + BehaviorValue bv = rv[0]; |
| + // If it is one-shot, remove it from the list. |
| + if (bv.isOneShot) { |
| + rv.removeRange(0, 1); |
| + } |
| + // Log the method call and the response. |
| + log.add(new LogEntry(name, args, bv)); |
| + // Do the response. |
| + if (bv.isThrow) { |
| + throw bv.value; |
| + } else { |
| + return bv.value; |
| + } |
| + } |
| + } |
| + throw new Exception('No behavior specified for method $name'); |
| + } |
| + |
| + /** |
| + * [verify] extracts all calls from the object log that match the |
| + * method signature, then applies the [was] matcher. The matching |
| + * list of [LogEntry]s is returned so that further calls to verify() |
| + * can be chained . |
| + */ |
| + LogEntryList verify(String method, [ arg0 = noArg, |
| + arg1 = noArg, |
| + arg2 = noArg, |
| + arg3 = noArg, |
| + arg4 = noArg, |
| + arg5 = noArg, |
| + arg6 = noArg, |
| + arg7 = noArg, |
| + arg8 = noArg, |
| + arg9 = noArg, |
| + Matcher was = calledOnce]) { |
| + CallMatcher logfilter = new CallMatcher(method, |
| + arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); |
| + LogEntryList _logs = log.getMatches(logfilter); |
| + _logs.verify(was); |
| + return _logs; |
| + } |
| + |
| + /** [verifyZeroInteractions] returns true if no calls were made */ |
| + bool verifyZeroInteractions() => log.logs.length == 0; |
| + |
| +} |
| + |