Testing in Go: Test Doubles by Example
Table of Contents
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.
Dummies #
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
slice of Person
as an attribute. Each Person
has a FirstName
, LastName
,
and 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.
The 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.
package main
import (
"errors"
)
var (
ErrMissingArgs = errors.New("FirstName and LastName are mandatory arguments")
ErrNoPersonFound = errors.New("No person found")
)
type Searcher interface {
Search(people []*Person, firstName string, lastName string) *Person
}
type Person struct {
FirstName string
LastName string
Phone string
}
type Phonebook struct {
People []*Person
}
func (p *Phonebook) Find(searcher Searcher, firstName, lastName string) (string, error) {
if firstName == "" || lastName == "" {
return "", ErrMissingArgs
}
person := searcher.Search(p.People, firstName, lastName)
if person == nil {
return "", ErrNoPersonFound
}
return person.Phone, nil
}
Imagine we want to test the Find
method of the Phonebook
. How would you
approach it? Here’s one way:
type DummySearcher struct{}
func (ds DummySearcher) Search(people []*Person, firstName, lastName string) *Person {
return &Person{}
}
func TestFindReturnsError(t *testing.T) {
phonebook := &Phonebook{}
want := ErrMissingArgs
_, got := phonebook.Find(DummySearcher{}, "", "")
if got != want {
t.Errorf("Want '%s', got '%s'", want, got)
}
}
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
complain.
Such instances are called dummies. They are test doubles that have no functionality and that we don’t want people to use.
Stubs #
Using the same example from above, how would we test that the Find
method
when supplied the firstName
and lastName
arguments will work as expected?
type StubSearcher struct {
phone string
}
func (ss StubSearcher) Search(people []*Person, firstName, lastName string) *Person {
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: ss.phone,
}
}
func TestFindReturnsPerson(t *testing.T) {
fakePhone := "+31 65 222 333"
phonebook := &Phonebook{}
phone, _ := phonebook.Find(StubSearcher{fakePhone}, "Jane", "Doe")
if phone != fakePhone {
t.Errorf("Want '%s', got '%s'", fakePhone, phone)
}
}
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.
Spies #
Again, using the Phonebook
and 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?
type SpySearcher struct {
phone string
searchWasCalled bool
}
func (ss *SpySearcher) Search(people []*Person, firstName, lastName string) *Person {
ss.searchWasCalled = true
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: ss.phone,
}
}
func TestFindCallsSearchAndReturnsPerson(t *testing.T) {
fakePhone := "+31 65 222 333"
phonebook := &Phonebook{}
spy := &SpySearcher{phone: fakePhone}
phone, _ := phonebook.Find(spy, "Jane", "Doe")
if !spy.searchWasCalled {
t.Errorf("Expected to call 'Search' in 'Find', but it wasn't.")
}
if phone != fakePhone {
t.Errorf("Want '%s', got '%s'", fakePhone, phone)
}
}
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.
Mocks #
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:
type MockSearcher struct {
phone string
methodsToCall map[string]bool
}
func (ms *MockSearcher) Search(people []*Person, firstName, lastName string) *Person {
ms.methodsToCall["Search"] = true
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: ms.phone,
}
}
func (ms *MockSearcher) ExpectToCall(methodName string) {
if ms.methodsToCall == nil {
ms.methodsToCall = make(map[string]bool)
}
ms.methodsToCall[methodName] = false
}
func (ms *MockSearcher) Verify(t *testing.T) {
for methodName, called := range ms.methodsToCall {
if !called {
t.Errorf("Expected to call '%s', but it wasn't.", methodName)
}
}
}
func TestFindCallsSearchAndReturnsPersonUsingMock(t *testing.T) {
fakePhone := "+31 65 222 333"
phonebook := &Phonebook{}
mock := &MockSearcher{phone: fakePhone}
mock.ExpectToCall("Search")
phone, _ := phonebook.Find(mock, "Jane", "Doe")
if phone != fakePhone {
t.Errorf("Want '%s', got '%s'", fakePhone, phone)
}
mock.Verify(t)
}
This approach is more involved, but there’s no magic to it. The MockSearcher
implementation has a methodsToCall
map, which will store all of the methods
that we expect to call on an instance of this type.
The 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).
In MockSearcher
’s 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 true
here.
Lastly, the 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.
Fakes #
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 Search
method.
Let’s see a Searcher
fake in action:
type FakeSearcher struct{}
func (fs FakeSearcher) Search(people []*Person, firstName string, lastName string) *Person {
if len(people) == 0 {
return nil
}
return people[0]
}
func TestFindCallsSearchAndReturnsEmptyStringForNoPerson(t *testing.T) {
phonebook := &Phonebook{}
fake := &FakeSearcher{}
phone, _ := phonebook.Find(fake, "Jane", "Doe")
if phone != "" {
t.Errorf("Wanted '', got '%s'", phone)
}
}
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
will return nil
. Otherwise, it will return the first item in the slice.
While this 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.
Additional reading:
- “The Little Mocker” by Uncle Bob
- “TestDouble” by Martin Fowler
- “Testing on the Toilet: Know Your Test Doubles” by Andrew Trenk