Mockito provides its stubbing and verification features by overriding class instance methods. Since there is no mechanism for overriding static methods, constructors, or top-level functions, mockito cannot mock them. They are what they are.
One idea to consider is "Do I need to use mocks to test this code?" For
example, the test_descriptor package allows testing file system concepts using
real files, and the test_process package supports testing subprocesses using
real subprocesses. dart:io
also includes an IOOverrides class and a
runWithIOOverrides function that can be used to mock out the filesystem.
If mocking is still desired, the underlying code may be refactored in order to enable mocking. One way to get around un-mockable constructors is to change the function in which the constructor is being called. Instead of constructing an object, accept one.
// BEFORE:
void f() {
var foo = Foo();
// ...
}
// AFTER
void f(Foo foo) {
// ...
}
In tests, you can declare a MockFoo class which implements Foo, and pass such
an object to f
.
You can also refactor code which makes much use of such constructors or static
methods to use a wrapper system. For example, instead of calling
Directory.current
or new File
throughout your code, use the
file package. You can start with a FileSystem object (a LocalFileSystem
for production and a MemoryFileSystem for tests), and use its wrapper methods
(currentDirectory
replaces Directory.current
, file()
replaces
File()
). Another example of this pattern is the io package and its
ProcessManager class.
If there is no way to override some kind of function, then mockito cannot mock it. See the above answer for further explanation, and alternatives.
When mockito verifies a method call (via verify
or verifyInOrder
), it
marks the call as "verified", which excludes the call from further
verifications. For example:
cat.eatFood("fish");
verify(cat.eatFood("fish")); // This call succeeds.
verify(cat.eatFood(any)); // This call fails.
In order to make multiple reasonings about a call, for example to assert on the arguments, make one verification call, and save the captured arguments:
cat.hunt("home", "birds");
var captured = verify(cat.hunt(captureAny, captureAny)).captured.single;
expect(captured[0], equals("home"));
expect(captured[1], equals("birds"));
If you need to verify the number of types a method was called, and capture the arguments, save the verification object:
cat.hunt("home", "birds");
cat.hunt("home", "lizards");
var verification = verify(cat.hunt("home", captureAny));
verification.called(greaterThan(2));
var firstCall = verification.captured[0];
var secondCall = verification.captured[1];
expect(firstCall, equals(["birds"]));
expect(secondCall, equals(["lizards"]));
The basics of the Mock
class are nothing special: It uses noSuchMethod
to
catch all method invocations, and returns the value that you have configured
beforehand with when()
calls.
The implementation of when()
is a bit more tricky. Take this example:
// Unstubbed methods return null:
expect(cat.sound(), nullValue);
// Stubbing - before execution:
when(cat.sound()).thenReturn("Purr");
Since cat.sound()
returns null
, how can the when()
call configure it?
It works, because when
is not a function, but a top level getter that
returns a function. Before returning the function, it sets a flag
(_whenInProgress
), so that all Mock
objects know to return a "matcher"
(internally _WhenCall
) instead of the expected value. As soon as the function
has been invoked _whenInProgress
is set back to false
and Mock objects
behave as normal.
Argument matchers work by storing the wrapped arguments, one after another,
until the when
(or verify
) call gathers everything that has been stored,
and creates an InvocationMatcher with the arguments. This is a simple process
for positional arguments: the order in which the arguments has been stored
should be preserved for matching an invocation. Named arguments are trickier:
their evaluation order is not specified, so if Mockito blindly stored them in
the order of their evaluation, it wouldn't be able to match up each argument
matcher with the correct name. This is why each named argument matcher must
repeat its own name. foo: anyNamed('foo')
tells Mockito to store an argument
matcher for an invocation under the name 'foo'.
Be careful never to write
when;
(without the function call) anywhere. This would set_whenInProgress
totrue
, and the next mock invocation will return an unexpected value.
The same goes for "chaining" mock objects in a test call. This will fail:
var mockUtils = MockUtils();
var mockStringUtils = MockStringUtils();
// Setting up mockUtils.stringUtils to return a mock StringUtils implementation
when(mockUtils.stringUtils).thenReturn(mockStringUtils);
// Some tests
// FAILS!
verify(mockUtils.stringUtils.uppercase()).called(1);
// Instead use this:
verify(mockStringUtils.uppercase()).called(1);
This fails, because verify
sets an internal flag, so mock objects don't
return their mocked values anymore but their matchers. So
mockUtils.stringUtils
will not return the mocked stringUtils
object you
put inside.
You can look at the when
and Mock.noSuchMethod
implementations to see how
it's done. It's very straightforward.