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

Unified Diff: lib/unittest/mock.dart

Issue 10679005: Initial version of mocking support. There is still quite a bit of (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 8 years, 6 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 side-by-side diff with in-line comments
Download patch
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;
+
+}
+

Powered by Google App Engine
This is Rietveld 408576698