Interesting objects collaborate, but some collaborators are tricky to work with.
When objects have awkward collaborators, test doubles (stub, fake, and mock objects) replace the collaborators to make testing easier.
Stub, fake, mock; different people interpret these labels differently; I’ll use the names we use at Industrial Logic and you can substitute the names you prefer:)
Interesting objects collaborate, but awkward collaborators make testing tricky.
We’ll walk through a series of pictures that show how microtests work with various test doubles.
Microtests usually follow the Arrange-Act-Assert pattern: Arrange some context (e.g., create and connect objects), Act to trigger some behavior, then Assert to check that the effects of that behavior are what you expected. You’ll see that pattern reflected in the pictures.
A Basic Test (“State-Based”)
Let’s start with a simple test, with no test doubles involved:
Arrange: Set up the test context: create an object and its collaborators (if any), and connect them together.
Act: The test triggers some behavior in the object being being tested.
Interaction: The object under test does its job, collaborating with other objects as it should.
Assert: The test queries the object under test and/or its collaborators to check that the action had the desired effect.
This style is sometimes called state-based since the assertions are often about the observable state of some objects.
Depending on your testing style, you may find that the majority of your tests are like this, and you only go to other styles when this doesn’t suffice.
Testing with Stubs
Some collaborators are troublemakers: they have side effects, or return random results, or require a chain of other collaborators. For whatever reason, we don’t want to use the real (production) collaborator.
In the simplest case, we can replace use a stub, a collaborator designed to do the bare minimum. Typically, it returns a constant for any return value, and ignores calls that don’t need return values.
The steps are the same as before: Arrange, Act, Interaction (behind the scenes), and Assert. The only difference is there’s no point querying the stub as it doesn’t know anything.
A stub returning the same value every time may not be able to trigger interesting behavior in the object being tested. In that case, A talking fake can be designed to return values one at a time from a list.
The test still has the same Arrange-Act-Assert structure; as with stubs, there’s no point querying the fake:
A talking fake simulates one side of the conversation but, in testing as in life, listening may be what’s important.
A listening fake records what the tested object tells it by saving the parameter values passed into calls on the fake and/or remembering which calls were made.
Our test structure is like the original test, with two key shifts:
- The test asserts about the data the listening fake remembered
- This data is about the conversation between objects, not the effects on the tested object
This style of testing (and even more when using mocks) is sometimes called interaction-based, because it looks at the interaction between objects rather than the results of their collaboration.
As we’ll see next, a listening fake is at least halfway to being a mock object.
If you re-use a listening fake object for several different tests, you may notice that the assertions are very similar.
To clean up duplication among the test methods, extract out a method with common assertions. Notice that the assertions are about the fake, a feature envy smell.
So, move the assertions to the fake, and you have created a mock object.
The test structure has changed somewhat:
- Arranging is more complicated: we need to configure the mock as well as create and connect objects. This may involve setting up return values (as for a talking fake), or telling the mock what to expect in terms of method calls and their arguments (as for a listening fake).
- The assertions have moved to the mock; the assertion step merely tells the mock to check itself.
- You can mix and match: you can still assert on the object being tested or its other collaborators.
Notice how the assertions have changed from our first test: rather than check the effects of an interaction, they check that the interaction between objects went as expected.
You can create a mock by hand, or use frameworks such as Mockito or xMock that will do it for you.
We’ve evolved from basic tests to more intricate tests that use mock objects.
While the overall test structure stays similar, mock objects shift from asserting about results to asserting that the interaction with a collaborator occurred as intended.