DartMocks is a mock framework for Dart inspired by RSpec. It's built on top of unittest/mock
.
Add the DartMocks dependency to your project’s pubspec.yaml.
name: my_project
dependencies:
dartmocks: any
Then, run pub install
.
Finally, import the unittest and dartmocks libraries.
import 'package:unittest/unittest.dart';
import 'package:dartmocks/dartmocks.dart';
In all the samples below I am going to test the following two classes:
class Player {
var currentValue = 0;
var isOn = true;
changeVolume(delta) => currentValue += delta;
}
class RemoteControl {
var player;
RemoteControl(this.player);
turnUp() => (player.isOn) ? player.changeVolume(10) : 0;
turnDown() => (player.isOn) ? player.changeVolume(-10) : 0;
}
You can pass a configuration object, which maps function names to their return values, to the stub function.
test("stubbing a turned off player", (){
var player = stub({"get isOn" : false});
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(0));
});
test("stubbing a turned on player", (){
var player = stub("Player", {"changeVolume" : 100, "get isOn" : true});
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(100));
});
Note, that you can optionally pass a name to make error messages more descriptive.
In addition to passing a map to the stub
function, you can configure each method individually.
test("stubbing a turned off player", (){
var player = stub("Player")
..stub("get isOn").andReturn(false);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(0));
});
You can specify the argument of a stubbed method call as follows:
test("specifying arguments", (){
var player = stub()
..stub("get isOn").andReturn(true)
..stub("changeVolume").args(10).andReturn(100)
..stub("changeVolume").args(-10).andReturn(-100);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(100));
expect(remote.turnDown(), equals(-100));
});
Configuring a stubbed method to throw an exception is done as follows:
test("throwing an exception", (){
var player = stub("Player")
..stub("get isOn").andThrow("BOOM!");
var remote = new RemoteControl(player);
expect(remote.turnUp, throws);
});
Custom behaviour is specified as follows:
test("calling custom functions", (){
var player = stub("Player")
..stub("get isOn").andReturn(true)
..stub("changeVolume").andCall((delta) => delta * 100);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(1000));
});
If you pass more than one argument to andReturn
, andCall
, or andThrow
, DartMocks will build a sequence of behaviours.
test("returning multiple values", (){
var player = stub("Player")
..stub("get isOn").andReturn(true)
..stub("changeVolume").andReturn(10,20,30);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(10));
expect(remote.turnUp(), equals(20));
expect(remote.turnUp(), equals(30));
//expect(remote.turnUp(), equals(40)); Will throw: No more actions for method changeVolume.
});
Note, that the stub throws an exception because the changeVolume
method sort of “ran out” of return values.
A partial test double, being an anti pattern, can still be useful in some situations. To create a partial test double just set the real
property on a test double.
test("partial stubs", (){
var realPlayer = new Player()..isOn=false;
var playerThatIsAlwaysOn = stub("Partial")
..real = realPlayer
..stub("get isOn").andReturn(true);
expect(playerThatIsAlwaysOn.isOn, equals(true));
expect(playerThatIsAlwaysOn.changeVolume(100), equals(100));
});
If you don't want your test double to respond to framework methods (e.g., stub
or shouldReceive
), call pure on it. The pure
method returns a test double that can only responds to the messages you configured.
test("pure", (){
var player = stub({"get isOn" : true});
var pure = player.pure();
expect(pure.isOn, equals(true));
//player.stub will work
expect(() => pure.stub("blah"), throws);
});
test("setting expectations", (){
var player = mock()
..shouldReceive("get isOn").andReturn(false);
var remote = new RemoteControl(player);
remote.turnUp();
player.verify();
});
The verify
method will check if all the set expectations have been met.
Calling verify on every created mock can be tedious. To help you with that, DartMocks stores all expectations. So you can check all of them at once by calling currentTestRun.verify()
.
tearDown((){
currentTestRun.verify();
});
test("setting expectations", (){
var player = mock()
..shouldReceive("get isOn").andReturn(false);
var remote = new RemoteControl(player);
remote.turnUp();
});
DartMocks allows you to specify how many times a particular method should be called.
test("specifying the number of calls", (){
var player = mock()
..shouldReceive("get isOn").andReturn(false).times(2);
var remote = new RemoteControl(player);
remote.turnUp();
remote.turnUp();
});
Both the mock
and stub
functions return an instance of TestDouble
. Which means that you can use the TestDouble
class directly.
test("using TestDouble", (){
var player = new TestDouble()
..name = "Player"
..stub("get isOn").andReturn(false);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(0));
});
There are a few good reasons to implement an interface (e.g., tooling). It can be done as follows:
class TestDoublePlayer extends TestDouble implements Player {}
test("test doubles implementing interfaces", (){
Player player = new TestDoublePlayer()
..stub("get isOn").andReturn(false);
var remote = new RemoteControl(player);
expect(remote.turnUp(), equals(0));
});
The initial version of DartMocks was written by Sean Kirby and me at one of hackathons organized by Nulogy.