Chromium Code Reviews| 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]) { |
| + 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. |
| + */ |
| +class _ResultMatcher extends BaseMatcher { |
| + final int action; |
| + final value; |
| + final int count; // -1 for all, 0 for none, 1 for some. |
| + |
| + 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); |
| + } |
| + 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], |
| + * [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); |
| + * |
| + * [thenCall] and [alwaysCall] allow you to proxy mocked methods, chaining |
| + * to some other implementation. This provides a way to implement 'spies'. |
| + * |
| * 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 |
| + * 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(...); |
|
Siggi Cherem (dart-lang)
2012/06/28 17:51:43
should we keep this example below?
getLogs(mock,
|
| */ |
| - 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; |
| - |
| } |
| + |