Dart API ReferenceunittestMock

Mock Class

Mock is the base class for all mocked objects, with 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, 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.when(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 getLogs to extract a relevant subset of method call logs and apply Matchers to these through calling verify.

A Mock can be given a name when constructed. In this case instead of keeping its own log, it uses a shared log. This can be useful to get an audit trail of interleaved behavior. It is the responsibility of the user to ensure that mock names, if used, are unique.

Limitations: - only positional parameters are supported (up to 10); - to mock getters you will need to include parentheses in the call

  (e.g. m.length() will work but not m.length).

Here is a simple example:

class MockList extends Mock implements List {};
List m = new MockList();
m.when(callsTo('add', anything)).alwaysReturn(0);
m.add('foo');
m.add('bar');
getLogs(m, callsTo('add', anything)).verify(happenedExactly(2));
getLogs(m, callsTo('add', 'foo')).verify(happenedOnce);
getLogs(m, callsTo('add', 'isNull)).verify(neverHappened);

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.when(callsTo('add')).alwaysReturn(0);
m.add(1, 2);

But this is not allowed and will throw an exception:

m.when(callsTo('add', anything, anything)).alwaysReturn(0);
m.add(1);

Here is a way to implement a 'spy', which is where we log the call but then hand it off to some other function, which is the same method in a real instance of the class being mocked:

class Foo {
  bar(a, b, c) => a + b + c;
}
class MockFoo extends Mock implements Foo {
  Foo real;
  MockFoo() {
    real = new Foo();
    this.when(callsTo('bar')).alwaysCall(real.bar);
  }
}

Constructors

Code new Mock.custom([String name, LogEntryList log, throwIfNoBehavior = false, enableLogging = true]) #

This constructor makes a mock that has a name and possibly uses a shared log. If throwIfNoBehavior is true, any calls to methods that have no defined behaviors will throw an exception; otherwise they will be allowed and logged (but will not do anything). If enableLogging is false, no logging will be done initially (whether or not a log is supplied), but logging can be set to true later.

Mock.custom([this.name,
             this.log,
             throwIfNoBehavior = false,
             enableLogging = true]) : _throwIfNoBehavior = throwIfNoBehavior {
  if (log != null && name == null) {
    throw new Exception("Mocks with shared logs must have a name.");
  }
  logging = enableLogging;
  _behaviors = new Map<String,Behavior>();
}

Code new Mock() #

Default constructor. Unknown method calls are allowed and logged, the mock has no name, and has its own log.

Mock() : _throwIfNoBehavior = false, log = null, name = null {
  logging = true;
  _behaviors = new Map<String,Behavior>();
}

Methods

Code LogEntryList calls(method, [arg0 = _noArg, arg1 = _noArg, arg2 = _noArg, arg3 = _noArg, arg4 = _noArg, arg5 = _noArg, arg6 = _noArg, arg7 = _noArg, arg8 = _noArg, arg9 = _noArg]) #

Useful shorthand method that creates a CallMatcher from its arguments and then calls getLogs.

LogEntryList calls(method,
                    [arg0 = _noArg,
                     arg1 = _noArg,
                     arg2 = _noArg,
                     arg3 = _noArg,
                     arg4 = _noArg,
                     arg5 = _noArg,
                     arg6 = _noArg,
                     arg7 = _noArg,
                     arg8 = _noArg,
                     arg9 = _noArg]) =>
    getLogs(callsTo(method, arg0, arg1, arg2, arg3, arg4,
        arg5, arg6, arg7, arg8, arg9));

Code LogEntryList getLogs([CallMatcher logFilter, Matcher actionMatcher, bool destructive = false]) #

getLogs extracts all calls from the call log that match the logFilter, and returns the matching list of LogEntrys. If destructive is false (the default) the matching calls are left in the log, else they are removed. Removal allows us to verify a set of interactions and then verify that there are no other interactions left. actionMatcher can be used to further restrict the returned logs based on the action the mock performed. logFilter can be a CallMatcher or a predicate function that takes a LogEntry and returns a bool.

Typical usage:

getLogs(callsTo(...)).verify(...);
LogEntryList getLogs([CallMatcher logFilter,
                      Matcher actionMatcher,
                      bool destructive = false]) {
  if (log == null) {
    // This means we created the mock with logging off and have never turned
    // it on, so it doesn't make sense to get logs from such a mock.
    throw new
        Exception("Can't retrieve logs when logging was never enabled.");
  } else {
    return log.getMatches(name, logFilter, actionMatcher, destructive);
  }
}

Code bool get logging() #

bool get logging() => _logging;

Code set logging(bool value) #

set logging(bool value) {
  if (value && log == null) {
    log = new LogEntryList();
  }
  _logging = value;
}

Code noSuchMethod(String method, List args) #

This is the handler for method calls. We loo through the list of Behaviors, 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 method, List args) {
  if (method.startsWith('get:')) {
    method = 'get ${method.substring(4)}';
  }
  bool matchedMethodName = false;
  MatchState matchState = new MatchState();
  for (String k in _behaviors.getKeys()) {
    Behavior b = _behaviors[k];
    if (b.matcher.nameFilter.matches(method, matchState)) {
      matchedMethodName = true;
    }
    if (b.matches(method, args)) {
      List actions = b.actions;
      if (actions == null || actions.length == 0) {
        continue; // No return values left in this Behavior.
      }
      // Get the first response.
      Responder response = 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 (--response.count == 0) {
        actions.removeRange(0, 1);
      }
      // Do the response.
      Action action = response.action;
      var value = response.value;
      if (action == Action.RETURN) {
        if (_logging) {
          log.add(new LogEntry(name, method, args, action, value));
        }
        return value;
      } else if (action == Action.THROW) {
        if (_logging) {
          log.add(new LogEntry(name, method, args, action, value));
        }
        throw value;
      } else if (action == Action.PROXY) {
        var rtn;
        switch (args.length) {
          case 0:
            rtn = value();
            break;
          case 1:
            rtn = value(args[0]);
            break;
          case 2:
            rtn = value(args[0], args[1]);
            break;
          case 3:
            rtn = value(args[0], args[1], args[2]);
            break;
          case 4:
            rtn = value(args[0], args[1], args[2], args[3]);
            break;
          case 5:
            rtn = value(args[0], args[1], args[2], args[3], args[4]);
            break;
          case 6:
            rtn = value(args[0], args[1], args[2], args[3],
                args[4], args[5]);
            break;
          case 7:
            rtn = value(args[0], args[1], args[2], args[3],
                args[4], args[5], args[6]);
            break;
          case 8:
            rtn = value(args[0], args[1], args[2], args[3],
                args[4], args[5], args[6], args[7]);
            break;
          case 9:
            rtn = value(args[0], args[1], args[2], args[3],
                args[4], args[5], args[6], args[7], args[8]);
            break;
          case 9:
            rtn = 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.");
        }
        if (_logging) {
          log.add(new LogEntry(name, method, args, action, rtn));
        }
        return rtn;
      }
    }
  }
  if (matchedMethodName) {
    // User did specify behavior for this method, but all the
    // actions are exhausted. This is considered an error.
    throw new Exception('No more actions for method '
        '${_qualifiedName(name, method)}.');
  } else if (_throwIfNoBehavior) {
    throw new Exception('No behavior specified for method '
        '${_qualifiedName(name, method)}.');
  }
  // Otherwise user hasn't specified behavior for this method; we don't throw
  // so we can underspecify.
  if (_logging) {
    log.add(new LogEntry(name, method, args, Action.IGNORE));
  }
}

Code bool verifyZeroInteractions() #

verifyZeroInteractions returns true if no calls were made

bool verifyZeroInteractions() {
  if (log == null) {
    // This means we created the mock with logging off and have never turned
    // it on, so it doesn't make sense to verify behavior on such a mock.
    throw new
        Exception("Can't verify behavior when logging was never enabled.");
  }
  return log.logs.length == 0;
}

Code Behavior when(CallMatcher logFilter) #

when is used to create a new or extend an existing Behavior. A [CallMatcher] [filter] must be supplied, and the Behaviors for that signature are returned (being created first if needed).

Typical use case:

mock.when(callsTo(...)).alwaysReturn(...);
Behavior when(CallMatcher logFilter) {
  String key = logFilter.toString();
  if (!_behaviors.containsKey(key)) {
    Behavior b = new Behavior(logFilter);
    _behaviors[key] = b;
    return b;
  } else {
    return _behaviors[key];
  }
}

Fields

Code LogEntryList log #

The log of calls made. Only used if name is null.

LogEntryList log;

Code final String name #

The mock name. Needed if the log is shared; optional otherwise.

final String name;