Creating test doubles in Go, manual or auto-generated?

We set up test doubles when it is difficult or impossible to use real objects due to complexity or external dependencies. In Go, an interface is a collection of method signatures that define a set of behaviors. With interfaces, it is easy to create test doubles in Go. However, should you write test doubles on your own, use a test library that supports mock, or a tool that generates mock automatically? In this article, we use stub and mock interchangeably. See Appendix A for difference between mock and stub.

Assume we need to test the function Greet which uses a Greeter to greet. For the complete source code, see greet.go and greet_test.go.

1type Greeter interface {
2    Greeting(a string) (string, error)
3}
4 
5// Greet is the function we are going to test.
6func Greet(g Greeter, a string) bool {
7    _, err := g.Greeting(a)
8    return err == nil
9}

Option 1. Write stub manually.

First, write a stub struct that implements the Greeter interface with canned responses.

 1type GreetingStub struct {
 2    func(string) (string, error)
 3}
 4 
 5func (g GreetingStub) Greeting(a string) (string, error) {
 6    g.GreetingFunc != nil {
 7        g.GreetingFunc(a)
 8    }
 9    return "", errors.New("not implemented")
10}

Then, use the stub in test.

 1func Test_Greeting_UsingManualStub(t *testing.T) {
 2    g := GreetingStub{GreetingFunc: func(s string) (string, error) {
 3        switch s {
 4        case "hello":
 5            return "world", nil
 6        case "HELLO":
 7            return "", errors.New("uppercase nota allowed")
 8        default:
 9            return "", nil
10        }
11    }}
12    assert.True(t, Greet(g, "hello"))
13    assert.False(t, Greet(g, "HELLO"))
14}

Option 2. Use testify mock

The testify mock provides a mechanism for easily writing mock objects /that can be used in place of real objects when writing test code.

Create a struct embedding mock.Mock and implement the Greeter interface.

1type GreetingMock struct {
2    mock.Mock
3}
4 
5func (m *GreetingMock) Greeting(a string) (string, error) {
6    args := m.Called(a)
7    return args.String(0), args.Error(1)
8}

Then we can use the mock in tests. Note that we can declare a method and its behaviors dynamically in each test. This is an advantage over the manual stub we saw in Option 1, where the stub struct defines behaviors statically. There are other features that Mock supports, e.g. method parameter matching.

1func Test_Greet_UsingTestifyMock(t *testing.T) {
2    m := new(GreetingMock)
3    m.On("Greeting", "hello").Return("world", nil)
4    m.On("Greeting", "HELLO").Return("", errors.New("uppercase nota allowed"))
5    assert.True(t, Greet(m, "hello"))
6    assert.False(t, Greet(m, "HELLO"))
7}

Option 3. Use mockery

The mockery provides the ability to easily generate mocks for Golang interfaces using the stretchr/testify/mock package. It removes the boilerplate code required to use mocks. There are just two steps to use mockery: annotate then generate.

Step 1. Annotate Go interface with “go generate” command.

1//go:generate mockery --name Greeter --filename greet_mock.go --inpackage-suffix --inpackage
2type Greeter interface {
3    Greeting(a string) (string, error)
4}

Step 2. Generate mock code. It will generate a file greet_mock.go as declared in the annotation above.

1go generate test-double-in-go/greet.go

Then we can use the mock in tests almost the same way we use the testify mock. With only one change. Replace m := new(GreetingMock) with m := NewMockGreeter(t).

So, which option to use?

  1. If you trust the open-source project, testify mock, then there is little reason to write stubs yourself.
  2. Use mockery if you are OK with mockery’s default behaviors. For example, mockery always assert expectations. If a method is declared, e.g., m.On("foo", arg1), then the method foo must be called with arg1.

Mockery can generate mock code as test files or as Go source files. If you generate mock code as Go source files, you can exclude those mock files from code coverage calculation.

1if [ "$(uname -s)" = "Darwin" ]; then
2  gsed -i '/.*_mock.go/d' "$coverage_file"
3else
4  sed -i '/.*_mock.go/d' "$coverage_file"
5fi

Appendix A. Mock or Stub?

When it comes to testing, both mocks and stubs are types of test doubles that can be used to isolate code under test from its dependencies. However, they serve slightly different purposes:

  1. A stub is a test double that provides canned responses to method calls. It is often used to simulate the behavior of external systems, such as databases or web services, and to test the interactions between code and those systems. Stubs are simpler than mocks, as they only provide predefined responses and do not verify that the code under test is interacting with the stub correctly.
  2. On the other hand, a mock is a test double that provides behavior that is defined by the test. It is often used to test interactions between objects, such as ensuring that a method is called with the correct parameters. Mocks are more complex than stubs, as they not only provide predefined responses, but also verify that the code under test is interacting with them correctly.

In summary, if you need to test interactions between code and external systems, a stub may be the appropriate choice. If you need to test interactions between objects or verify that a method is called with the correct parameters, a mock may be the better option.