One of the biggest misconceptions is that doubles are a specific implementation of mocks or other testing constructs that we use in testing.
Dummies, mocks, stubs, fakes, and spies ARE test doubles. Test double is the category of these test constructs. Over the years, there has been some confusion around this concept.
It is my observation that this confusion arises from the naming of testing constructs that the popular testing frameworks use. Also, the words mock and doubles have been used interchangeably over the years, and their definitions got skewed.
In any case, without further ado, let’s dive in and explore each of these categories of test doubles.
The simplest type of test double is a test dummy. In a nutshell, it means that the instance is just a placeholder for another one, but its functions do not return anything.
Let’s imagine the following scenario. We have a
Phonebook type that has a
Person as an attribute. Each
Person has a
Phone attributes. Also, the
Phonebook has a
Find method through which
we can find a person’s phone number using their first and last names.
Find method uses a
Searcher object whose
Search function it uses to
find the entry in the slice of people.
Searcher’s implementation is not
essential at this moment.
Imagine we want to test the
Find method of the
Phonebook. How would you
approach it? Here’s one way:
To test that the
Find method returns an error when one of the arguments is
blank, we do not care about the implementation of the
Searcher argument. In
such cases, where the functionality of the injected dependency is irrelevant,
all we need is an instance that we can just pass in, and the compiler won’t
Such instances are called dummies. They are test doubles that have no functionality and that we don’t want people to use.
Using the same example from above, how would we test that the
when supplied the
lastName arguments will work as expected?
When we want to make the
Searcher implementation return an actual value that
we can assert against, we need a stub. Instead of understanding what it would
take to set up and/or implement a proper
Searcher, you just create a stub
implementation that returns only one value. This is the idea behind stubs.
Again, using the
Searcher example from above, let’s imagine
we would like to write a test where we want to be sure we invoke the
Searcher.Search function. How can we do that?
You can think of spies as an upgrade of stubs. While they return a predefined value, just like stubs, spies also remember whether we called a specific method. Often, spies also keep track of how many times we call a particular function.
That’s what spy is - a stub that keeps track of invocations of its methods.
In the beginning, people started using mock for similar (but not the same) things, and the word got left hanging in the air without a proper definition. Some think of stubs as mocks; others do not even think of mocks as types of instances.
It’s generally accepted to use “mocking” when thinking about creating objects that simulate the behavior of real objects or units.
But mocks are a thing of their own. They have the same characteristics as the stubs & spies, with a bit more.
Also, in Go, they are a bit tricky to implement, especially in a generic way. Still, for this example, we will do a home-made implementation:
This approach is more involved, but there’s no magic to it. The
implementation has a
methodsToCall map, which will store all of the methods
that we expect to call on an instance of this type.
ExpectToCall method will take a method name as an argument. It will store
the method to the
methodsToCall map (as the key) and a
false as the value.
By setting it to
false, we set a mark on that method that we expect to call
it (yet we still haven’t called it).
Search method, we mark the
Search method as called. We
do this by setting the
true value for the
"Search" key in the
methodsToCall map. In essence, the key we’ve set to
false in the
ExpectToCall method we set to
Verify method will go over all of the methods that we marked as
to-be-called on the mock. If it finds one still set to
false, it will mark
the test as failed.
Mocks work by setting certain expectations on them. We implement some stub-like functionality while keeping track of the methods that have been called. Finally, we ask the mock to verify if our code met all of its expectations.
It’s worth stating that this is a home-made solution, and as such, it has some caveats. If you would like to use mocks in your tests, there are some excellent Golang libraries out there like golang/mock or stretchr/testify.
These test doubles, unlike stubs, spies, and mocks, truly have an
implementation. In our example, this would mean that a fake would have an
actual implementation of the
Let’s see a
Searcher fake in action:
What you see is a
FakeSearcher type, who’s
Search method has an
implementation that (kinda) makes sense. If the slice of
*Person is empty, it
nil. Otherwise, it will return the first item in the slice.
Search method is not production-ready, because it doesn’t make
much sense, it still has the behavior of a fake
Searcher. If we didn’t have
it’s implementation accessible (looking at it as a black box), one could think
it’s the real deal instead of a fake.
That’s what fakes are - implementations that look like the real thing. But they only do the trick in the scope of the test.
Another such familiar example in the community is an in-memory database driver used as fake to a real database driver.
When To Use What
Now that we went through all of the test doubles and their implementations, the last question that we need to answer is: what test double should we use in which circumstances?
As with many other things in software development, the answer is: it depends.
You must keep in mind is what are you really testing. That is also known as the unit under test. If the unit under test requires a double of any sort, you should provide the simplest test double that will make your test do its work and that it will suffice to make the test pass.
So, refrain from using bloated mocks if a stub does the trick. The rule of thumb is: use the simplest test double that you can. And after the test passes, see if you can refactor and simplify further.
And lastly, make sure not to downright mock out everything – sometimes invoking the actual implementation (e.g., like in an integration test) can be a great idea.
- “The Little Mocker” by Uncle Bob
- “TestDouble” by Martin Fowler
- “Testing on the Toilet: Know Your Test Doubles” by Andrew Trenk