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

Unified Diff: lib/unittest/mock.dart

Issue 10718002: Changed the syntax some. All the handling of variable argument lists is (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
« no previous file with comments | « no previous file | tests/lib/unittest/unittest_test.dart » ('j') | tests/lib/unittest/unittest_test.dart » ('J')
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: lib/unittest/mock.dart
===================================================================
--- lib/unittest/mock.dart (revision 9183)
+++ lib/unittest/mock.dart (working copy)
@@ -40,6 +40,11 @@
*/
final _noArg = const _Sentinel();
+/** The ways in which a call to a mock method can be handled. */
+final RETURN = 0;
+final THROW = 1;
+final PROXY = 2;
+
/**
* The behavior of a method call in the mock library is specified
* with [BehaviorValue]s. A [BehaviorValue] has a [value] to throw
@@ -49,9 +54,9 @@
*/
class BehaviorValue {
var value;
- bool isThrow;
+ int action;
int count;
- BehaviorValue(this.value, [this.count = 1, this.isThrow = false]);
+ BehaviorValue(this.value, [this.count = 1, this.action = RETURN]);
}
/**
@@ -122,10 +127,13 @@
* if it matches this [CallMatcher.
*/
bool matches(String method, List arguments) {
- if (method != this.name || arguments.length != argMatchers.length) {
+ if (method != this.name) {
return false;
}
- for (var i = 0; i < arguments.length; i++) {
+ if (arguments.length < argMatchers.length) {
+ throw new Exception("Less arguments than matchers for $name");
+ }
+ for (var i = 0; i < argMatchers.length; i++) {
if (!argMatchers[i].matches(arguments[i])) {
return false;
}
@@ -134,16 +142,31 @@
}
}
+/** [callstTo] returns a CallMatcher for the specified signature. */
+CallMatcher callsTo(String method, [ arg0 = _noArg,
+ arg1 = _noArg,
+ arg2 = _noArg,
+ arg3 = _noArg,
+ arg4 = _noArg,
+ arg5 = _noArg,
+ arg6 = _noArg,
+ arg7 = _noArg,
+ arg8 = _noArg,
+ arg9 = _noArg]) {
+ return new CallMatcher(method, arg0, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8, arg9);
+}
+
/**
* 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.
+ List<BehaviorValue> actions; // The values to return/throw or proxies to call.
Behavior (this.matcher) {
- returnValues = new List<BehaviorValue>();
+ actions = new List<BehaviorValue>();
}
/**
@@ -151,7 +174,7 @@
* times (1 by default).
*/
Behavior thenReturn(value, [count = 1]) {
- returnValues.add(new BehaviorValue(value, count));
+ actions.add(new BehaviorValue(value, count, RETURN));
return this; // For chaining calls.
}
@@ -165,7 +188,7 @@
* times (1 by default).
*/
Behavior thenThrow(value, [count = 1]) {
- returnValues.add(new BehaviorValue(value, count, true));
+ actions.add(new BehaviorValue(value, count, THROW));
return this; // For chaining calls.
}
@@ -174,6 +197,20 @@
return thenThrow(value, 0);
}
+ /**
+ * [thenCall] creates a proxy, that is called [count]
+ * times (1 by default).
+ */
+ Behavior thenCall(value, [count = 1]) {
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 what is [value]. Looking a the code in the test, s
gram 2012/06/28 17:32:54 Added a comment.
+ actions.add(new BehaviorValue(value, count, PROXY));
+ return this; // For chaining calls.
+ }
+
+ /** [alwaysCall] creates a repeating proxy call. */
+ Behavior alwaysCall(value) {
+ return thenCall(value, 0);
+ }
+
/** [matches] return true if a method call matches the [Behavior]. */
bool matches(name, args) => matcher.matches(name, args);
}
@@ -185,9 +222,10 @@
class LogEntry {
final String name; // The method name.
final List args; // The parameters.
- final BehaviorValue result; // The behavior that resulted.
+ final int action; // The behavior that resulted.
+ final value; // The value that was returned (if no throw).
- const LogEntry(this.name, this.args, this.result);
+ const LogEntry(this.name, this.args, this.action, [this.value = null]);
}
/**
@@ -205,15 +243,19 @@
/**
* Create a new [LogEntryList] consisting of [LogEntry]s from
- * this list that match the specified [logfilter].
+ * this list that match the specified [logfilter]. If [destructive]
+ * is true, the log entries are removed from the original list.
*/
- LogEntryList getMatches(CallMatcher logfilter) {
+ LogEntryList getMatches(CallMatcher logfilter, bool destructive) {
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);
+ if (destructive) {
+ logs.removeRange(i--, 1);
+ }
}
}
return rtn;
@@ -283,6 +325,112 @@
final Matcher calledAtMostOnce = const _TimesMatcher(0, 1);
/**
+ * [_ResultMatcher]s are used to make assertions about the results
+ * of method calls.
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 => of a sequence of method calls? (since this is a
gram 2012/06/28 17:32:54 I don't like 'sequence', even if the logs are temp
+ */
+class _ResultMatcher extends BaseMatcher {
+ final int action;
+ final value;
+ final int count; // -1 for all, 0 for none, 1 for some.
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 please change to something more self explanatory (
gram 2012/06/28 17:32:54 I used a single value as an enum; it scales better
+
+ const _ResultMatcher(this.action, this.value, this.count);
+
+ bool matches(log) {
+ for (LogEntry entry in log) {
+ // normalize the action; PROXY is like RETURN.
+ int eaction = (entry.action == THROW) ? THROW : RETURN;
+ if (eaction == action && value.matches(entry.value)) {
+ if (count == 0) {
+ return false;
+ } else if (count == 1) {
+ return true;
+ }
+ } else {
+ // Mismatch.
+ if (count == -1) { // We need just one mismatch to fail.
+ return false;
+ }
+ }
+ }
+ // If we get here, then if count is -1 we got all matches and
+ // this is success; otherwise we got all mismatched which is
+ // success for count == 0 and failure for count == 1.
+ return (count != 1);
+ }
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 + line separation between methods (here and below
gram 2012/06/28 17:32:54 Done.
+ Description describe(Description description) {
+ description.add(' to ');
+ description.add(count == -1 ? 'alway ' :
+ (count == 0 ? 'never ' : 'sometimes '));
+ if (action == RETURN || action == PROXY)
+ description.add('return ');
+ else
+ description.add('throw ');
+ return description.addDescriptionOf(value);
+ }
+ Description describeMismatch(log, Description mismatchDescription) {
+ if (count != 1) {
+ mismatchDescription.add(entry.value);
+ for (LogEntry entry in log) {
+ if (entry.action != action || !value.matches(entry.value)) {
+ if (entry.action == RETURN || entry.action == PROXY)
+ mismatchDescription.add('returned ');
+ else
+ mismatchDescription.add('threw ');
+ mismatchDescription.add(entry.value);
+ mismatchDescription.add(' at least once');
+ break;
+ }
+ }
+ } else {
+ mismatchDescription.add('never did');
+ }
+ return mismatchDescription;
+ }
+}
+
+/**
+ *[alwaysReturned] asserts that all matching calls to a method returned
+ * a value that matched [value].
+ */
+Matcher alwaysReturned(value) =>
+ new _ResultMatcher(RETURN, wrapMatcher(value), -1);
+
+/**
+ *[sometimeReturned] asserts that at least one matching call to a method
+ * returned a value that matched [value].
+ */
+Matcher sometimeReturned(value) =>
+ new _ResultMatcher(RETURN, wrapMatcher(value), 1);
+
+/**
+ *[neverReturned] asserts that no matching calls to a method returned
+ * a value that matched [value].
+ */
+Matcher neverReturned(value) =>
+ new _ResultMatcher(RETURN, wrapMatcher(value), 0);
+
+/**
+ *[alwaysThrew] asserts that all matching calls to a method threw
+ * a value that matched [value].
+ */
+Matcher alwaysThrew(value) =>
+ new _ResultMatcher(THROW, wrapMatcher(value), -1);
+
+/**
+ *[sometimeThrew] asserts that at least one matching call to a method threw
+ * a value that matched [value].
+ */
+Matcher sometimeThrew(value) =>
+ new _ResultMatcher(THROW, wrapMatcher(value), 1);
+
+/**
+ *[neverThrew] asserts that no matching call to a method threw
+ * a value that matched [value].
+ */
+Matcher neverThrew(value) =>
+ new _ResultMatcher(THROW, wrapMatcher(value), 0);
+
+/**
* [Mock] is the base class for all mocked objects, with
* support for basic mocking.
*
@@ -291,12 +439,21 @@
* 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].
+ * [whenThereAre] (to select the method and parameters) and [thenReturn],
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 whenThereAre feels really verbose, see some sugges
gram 2012/06/28 17:32:54 Done.
+ * [alwaysReturn], [thenThrow], [alwaysThrow], [thenCall] or [alwaysCall].
+ * [thenReturn], [thenThrow] and [thenCall] are one-shot so you would
+ * typically call these more than once to specify a sequence of actions;
+ * this can be done with chained calls, e.g.:
*
+ * m.whenThereAre(callsTo('foo')).
+ * thenReturn(0).thenReturn(1).thenReturn(2);
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 you can also use cascades here :) m.whenThereAre(c
gram 2012/06/28 17:32:54 Yeah, but most people are never going to know they
+ *
+ * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining
+ * to some other implementation. This provides a way to implement 'spies'.
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 It took me a while to get what are proxies and spi
gram 2012/06/28 17:32:54 I've changed comments in a few places that hopeful
+ *
* 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.
+ * behavior, use [forThe] to extract a relevant subset of method call
Siggi Cherem (dart-lang) 2012/06/28 00:44:40 :-( I liked verify better... more suggestions on t
+ * logs and apply [Matchers] to these through calling [verify].
*
* Limitations:
* - only positional parameters are supported (up to 10);
@@ -307,14 +464,40 @@
* class MockList extends Mock implements List {};
*
* List m = new MockList();
- * m.when('add', anything).alwaysReturn(0);
+ * m.whenThereAre(callsTo('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);
+ * m.forThe(callsTo('add', anything)).verify(calledExactly(2));
+ * m.forThe(callsTo('add', 'foo')).verify(calledOnce);
+ * m.forThe(callsTo('add', 'isNull)).verify(neverCalled);
+ *
+ * Note that we don't need to provide argument matchers for all arguments,
+ * but we do need to provide arguments for all matchers. So this is allowed:
+ *
+ * m.whenThereAre(callsTo('add')).alwaysReturn(0);
+ * m.add(1, 2);
+ *
+ * But this is not allowed and will throw an exception:
+ *
+ * m.whenThereAre(callsTo('add', anything, anything)).alwaysReturn(0);
+ * m.add(1);
+ *
+ * Here is a way to implement a 'spy':
+ *
+ * class Foo {
+ * bar(a, b, c) => a + b + c;
+ * }
+ *
+ * class MockFoo extends Mock implements Foo {
+ * Foo real;
+ * MockFoo() {
+ * real = new Foo();
+ * this.whenThereAre(callsTo('bar')).alwaysCall(real.bar);
+ * }
+ * }
+ *
*/
class Mock {
Map<String,Behavior> behaviors; /** The set of [behavior]s supported. */
@@ -326,27 +509,19 @@
}
/**
- * [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).
+ * [whenThereAre] is used to create a new or extend an existing [Behavior].
+ * A [CallMatcher] [filter] must be supplied, and the [Behavior]s for
+ * that signature are returned (being created first if needed).
+ *
+ * The name [whenThereAre] was chosen to flow grammatically with the
+ * typical use case:
+ *
+ * mock.whenThereAre(callsTo(...)).thenAlwaysReturn(...);
*/
- 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();
+ Behavior whenThereAre(CallMatcher filter) {
+ String key = filter.toString();
if (!behaviors.containsKey(key)) {
- Behavior b = new Behavior(logfilter);
+ Behavior b = new Behavior(filter);
behaviors[key] = b;
return b;
} else {
@@ -365,19 +540,19 @@
for (String k in behaviors.getKeys()) {
Behavior b = behaviors[k];
if (b.matches(name, args)) {
- List rv = b.returnValues;
- if (rv == null || rv.length == 0) {
+ List actions = b.actions;
+ if (actions == null || actions.length == 0) {
continue; // No return values left in this Behavior.
}
// Get the first response.
- BehaviorValue bv = rv[0];
+ BehaviorValue bv = actions[0];
// If it is exhausted, remove it from the list.
// Note that for endlessly repeating values, we started the count at
// 0, so we get a potentially useful value here, which is the
// (negation of) the number of times we returned the value.
if (--bv.count == 0) {
- rv.removeRange(0, 1);
- if (rv.length == 0) {
+ actions.removeRange(0, 1);
+ if (actions.length == 0) {
// Remove the behavior. Note that in the future there
// may be some value in preserving the behaviors for
// auditing purposes (e.g. how many times was this behavior used?).
@@ -386,13 +561,61 @@
behaviors.remove(k);
}
}
- // 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;
+ switch (bv.action) {
+ case RETURN:
+ log.add(new LogEntry(name, args, bv.action, bv.value));
+ return bv.value;
+ case THROW:
+ log.add(new LogEntry(name, args, bv.action, bv.value));
+ throw bv.value;
+ case PROXY:
+ var rtn;
+ switch (args.length) {
+ case 0:
+ rtn = bv.value();
+ break;
+ case 1:
+ rtn = bv.value(args[0]);
+ break;
+ case 2:
+ rtn = bv.value(args[0], args[1]);
+ break;
+ case 3:
+ rtn = bv.value(args[0], args[1], args[2]);
+ break;
+ case 4:
+ rtn = bv.value(args[0], args[1], args[2], args[3]);
+ break;
+ case 5:
+ rtn = bv.value(args[0], args[1], args[2], args[3], args[4]);
+ break;
+ case 6:
+ rtn = bv.value(args[0], args[1], args[2], args[3],
+ args[4], args[5]);
+ break;
+ case 7:
+ rtn = bv.value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6]);
+ break;
+ case 8:
+ rtn = bv.value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7]);
+ break;
+ case 9:
+ rtn = bv.value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7], args[8]);
+ break;
+ case 9:
+ rtn = bv.value(args[0], args[1], args[2], args[3],
+ args[4], args[5], args[6], args[7], args[8], args[9]);
+ break;
+ default:
+ throw new Exception(
+ "Cannot proxy calls with more than 10 parameters");
+ }
+ log.add(new LogEntry(name, args, bv.action, rtn));
+ return rtn;
}
}
}
@@ -400,31 +623,24 @@
}
/**
- * [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 .
+ * [forThe] extracts all calls from the object log that match the
+ * [logFilter] [CallMatcher], and returns the matching list of
+ * [LogEntry]s. If [destructive] is false (the default) the matching
+ * calls are left in the mock object's log, else they are removed.
+ * Removal allows us to verify a set of interactions and then verify
+ * that there are no other interactions left.
+ *
+ * The name [forThe] was chosen to flow grammatically with [callsTo],
+ * as this is usually used in the form:
+ *
+ * mock.forThe(callsTo(...)).verify(...);
*/
- 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;
+ LogEntryList forThe(CallMatcher logFilter, [bool destructive = false]) {
+ return log.getMatches(logFilter, destructive);
}
/** [verifyZeroInteractions] returns true if no calls were made */
bool verifyZeroInteractions() => log.logs.length == 0;
-
}
+
« no previous file with comments | « no previous file | tests/lib/unittest/unittest_test.dart » ('j') | tests/lib/unittest/unittest_test.dart » ('J')

Powered by Google App Engine
This is Rietveld 408576698