Skip to content

Test Doubles And Patching

Daniel Nephin edited this page May 1, 2022 · 1 revision

This page documents patterns for creating test doubles in Go. This page is intended for a reader that is already familiar with the Go testing package, and with writing tests in Go. To get started with testing in Go, check out the go.dev tutorial.

The patterns documented on this page are too small to be useful in a library. Often abstracting them into generic library code would make them harder to use. They are intended to be copied, or applied, in any package where they are used.

Know of a common Go testing pattern that you would like to add to this page? Please [open an issue] to suggest adding it!

Mocks, stubs, spies, fakes, and test doubles

Mocks, stubs, spies, and fakes are all kinds of test doubles. They are used in tests to replace the implementation of an interface to make code either easier to test, or to allow the test to run faster.

Using a test double can make writing tests easier and running tests faster, but overusing them can reduce the effectiveness of tests, and make tests more expensive to maintain. Always try to avoid a test double, and only use one when the alternative would significantly slow down the test.

Test doubles come in many forms, but all of them have one thing in common. They all implement some interface, and will be used in a test to replace the production implementation of that interface.

This common property of test doubles allows us to look at creating a test double in two stages.

Writing the method definitions

First we need to generate the method definitions to implement the interface.

There are plenty of libraries that provide either code generation, or building blocks for creating fakes and mocks. While those tools may be useful for faking large interfaces, it is often better to instead reduce the size of the interface. Most interfaces in the standard library have 3 or fewer methods. Reducing the size of the interface that needs to be faked is good for loose coupling of code, but it also makes the interface easier to implement.

A small interface can be written out by hand, but most IDEs ( GoLand, VS Code, gopls) provide one or more ways of easily generating the initial method stubs for a fake implementation. The code generation provided by the IDE often makes external tools unnecessary, but they are an option.

IDE refactoring tools will also take care of updating the method signatures when an interface changes, removing the need for an external tools to update the method signatures of the test double.

Implementing the interface

Now that the method definitions exist, we need to write the implementation of those methods.

A fake is a full working implementation, not something any tool can help implement.

A stub returns predefined return values, again not something any tool can help you implement. Add a few fields to the struct to store the predefined return values, and set the values as part of test setup.

A mock registers expectations about what calls will be made. Don't use mocks. If you need to record something about calls being made, use a spy instead and make the assertions about calls as you would any other assertion in a test.

A spy records both the method calls and all the arguments passed to those calls. It can also return a value using the same technique as a stub or fake.

Example: Spy

A test spy can easily be implemented without any library or framework using the following struct and function.

type FunctionCall struct {
    Name string
    Args []interface{}
}

func Record(method interface{}, args ...interface{}) FunctionCall {
    name := runtime.FuncForPC(reflect.ValueOf(method).Pointer()).Name()
    return FunctionCall{Name: name, Args: args}
}

From each fake method, you call Record, and at the end of the test case, use assert.DeepEqual to compare the recorded calls against the expected calls.

type testDouble struct {
    calls []FunctionCall
}
func (d *testDouble) Emit(arg1 string, arg2 time.Time) error {
    d.calls = append(d.calls, Record(d.Emit, arg1, arg))
    return nil
}

...

func TestEmit(t *testing.T) {
    d := &testDouble{}
    ...
    expected := []FunctionCall{
        Record(d.Emit, "metric", now),
    }
    assert.DeepEqual(t, d.calls, expected)
}

This pattern removes the need for a library, and ensures that assertion failures from the test double look and feel the same as other assertions in the test.

Monkey patching shims

A shim is a variable that is introduced into the program entirely for the purpose of being able to change it during testing. Shims are often used to replace a static function call to some external package with a different implementation of the function. Shims are only necessary when the dependency is static (i.e. the function being called is a package-level function, not a method on some type). Shims should only be used sparingly. If you can pass in an interface as a dependency, prefer that over a shim, as it is easier to pass in a fake implementation of the interface.

Once the shim is added, we can use a monkey patch function in a test to replace the shim with a fake implementation. The patch function can accept either a static value to return, or it could accept a full replacement for the function being patched. The examples below show both options.

Note: since patching is used to replace static values, it is never safe to use t.Parallel with patching.

Example: Generic Patch function

func Patch[S any](t *testing.T, target *S, replacement S) {
    original := *target
    *target = replacement
    t.Cleanup(func() {
        *target = original
    })
}

Example: time.Now

It is common to want to patch time.Now so that it returns a predictable value. This can make it much easier to test code that uses the current time.

In the live code introduce a shim.

// timeNow is a shim for testing
var timeNow = time.Now

// something is a function that uses time.Now
func something() string {
    now := timeNow()
    ...
}

In the test function patch timeNow. The patch function uses t.Cleanup to reset the value of the shim to the original value when the test function exits.

func TestSomething(t *testing.T) {
    now := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)
    patchTimeNow(t, now)

    // result should be predictable because we set a predictable value for now
    result := something()
}

func patchTimeNow(t *testing.T, now time.Date) {
    orig := timeNow
    timeNow = func() time.Time {
        return now
    }
    t.Cleanup(func() {
        timeNow = orig
    })
}

Example: Patching a function

Similar to the example above, the patch function could also accept a full replacement, instead of only accepting a static return value. This allows each test case to customize the value, or receive the value of the arguments passed to the function.

In the live code introduce a shim.

// runServer is a shim for testing
var runServer = server.Run

In the test code patch the shim to intercept the call, and use t.Cleanup to reset the shim when the test function exits.

func TestServerCmd(t *testing.T) {
    var actual server.Options
    patchRunServer(t, func(opts server.Options) error {
        actual = opts
        return nil
    })
    ...
}

func patchRunServer(t *testing.T, fn func(opts server.Options) error) {
    orig := runServer
    runServer = fn
    t.Cleanup(func() {
        runServer = orig
    })
}