There are times that code that needs to be unit tested contains code that interacts with things you do not want to run during a unit test. Interactions with expensive web services, hardware, production datastores, or even non-deterministic random number generators all make cases for testing that requires something to replace, for the purposes of the unit test, those things.
Of course, when you do want to test your code with those services running, you are then running integration tests.
The following approaches are for unit testing, with one (Monkey patching) included for completeness.
Note: All of the following approaches take advantage of the fact that the Go compiler will not include code in files with the suffix _test.go
or in packages with the suffix _test
into the normal build, the code will only be used in testing.
Dependency Injection
By far and away the best way to do this is to code to interfaces, the concrete implementations of which are determined at runtime via Dependency Injection. A struct type is created within the package that has interfaces within it that are set at runtime.
In this example the service
interface will be replaced in tests.
example.go
package mystuff
type service interface {
DoStuff() err
}
type Deps struct {
ServiceName service
}
func (d *Deps) RunService() error{
return d.Servicename.DoStuff()
}
It’s trivial to create something that satisfies the service
interface, and create an instance of the Deps
type that has your test service as a ServiceName
.
example_test.go
package mystuff_test
import (
"fmt"
"testing"
"github.com/example/mystuff"
"github.com/stretchr/testify/assert"
)
// Concrete implementation of interface
type MockService struct {}
// Let's control the error message in the tests
var errMsg string
func (m MockService) DoStuff() error {
return fmt.Errorf("returning %s", errMsg)
}
func TestSomething(t *testing.T){
// Create a Deps instance with our mocked service
m := MockService{}
d := mystuff.Deps{ServiceName: m}
// Set errMsg for this test
errMsg = "mock message"
// actual test
err := d.ServiceName.DoStuff()
// check that output matches what we are expecting
assert.EqualErrorf(t, err, "returning %s", errMsg)
}
This pattern can be used for fields that have a type that has a type […] that has an interface. For example Junos (a Juniper Network device API implementation for Go) has a pointer to a netconf.Session which has a netconf.Transport interface that can be replaced with a test implementation, and then all of the types above built in the test with it.
Because of the genius that is “implicit Go interface implementation” the fact that the interface is not exported does not prevent the tester from writing the code that implements a concrete type that satisfies the interface in the _test
package.
Assigning to a package variable
Another popular technique is to assign a method to a variable that can then be replaced in the tests. This method requires some of the test code to be in the same package as that being tested but in a _test.go
file.
example.go
package mystuff
import (
"math/rand"
)
var randIntn = rand.Intn
func UseRandom(n int) int {
return randIntn(n)
}
example_test.go
package mystuff
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUseRandom(t *testing.T){
// set randIntn to be a function where the output is deterministic
randIntn = func(n int)int {return n}
// RESET randIntn when this function exits in case other tests rely on a random value to be generated
defer randIntn = rand.Intn
// Call UseRandom and check that the output is what we expect
testVal := 100
output := UseRandom(testVal)
assert.Equal(t, output, testVal, "was expecting values to equal")
}
The randIntn
variable is set to be the rand.Intn
method in production code, but during the test it is set to the anonymous function that returns a deterministic value to allow testing. If this wasn’t done, there’d be no way to check if the rand.Intn was what was returned because there’s no way to generate the value in a way that can be seen outside the function being tested, and be used by that function.
Monkey Patching
This is only provided for completeness. Assigning the method that we need to control to a variable is far superior for testing.
Referring to buok/monkey a test can replace any function/method arbitrarily within the test (monkey patching can occur in production code, but it’s inherently dangerous and leads to unspecified behaviour) in much the same way as assigning the method to a variable, with the exception that the developer of the system being tested does not need to assign the method to any variable in advance.
This means that monkey patching is useful when testing code that uses a method that is deep inside a 3rd party package, or even the standard library for Go, with the only rule being that the method being monkey patched needs to be visible (aka exported).
Summary
Go lends itself well to techniques like Dependency Injection and replacing methods during testing using variables, and when used properly almost eliminate any reason not to attain coverage of code exceeding 100%.