In parts 1-3 of this tutorial series, we saw how you can use Go to write automated tests. Go's testing is very conducive to giving test coverage to a lot of your codebase's functionality. However, there's one area in particular where it's harder to do automated tests: code that is nondeterministic.
When I say nondeterministic code, I am talking about code where you don't have total control over the code's logic and output. This can be things like:
- 🧮 Code that uses pseudorandom number generators, like
math/rand
- 💻 Web API calls and their response payloads, or errors
- ⏰ The current time returned by
time.Now
Luckily, one tool that helps with testing nondeterministic code, is the mock
package of the popular Testify testing framework, which you can use for mocking out these API calls.
For this tutorial, we'll look at how to mock out code that uses math/rand
, then in a follow-up post, we'll mock out a fake web API client to see how to use Testify Mock in more complex scenarios.
Prerequisites for this tutorial are familiarity with the basics of writing and running a Go test, which the first tutorial in this series covers.
🎰 Getting some nondeterministic code and the Testify Mock package
Probably the easiest nondeterministic code to get ahold of is the math/rand
package, so for this tutorial, we'll look at how we would use Testify Mock to test code that uses randomness. We'll make a function that takes in an integer, and integer-divides (no decimal point) it by a number between 1 and 10.
package main
import (
"math/rand"
)
func divByRand(numerator int) int {
return numerator / int(rand.Intn(10))
}
Pretty simple function, but since we called rand.Intn
, which takes in a maximum value and returns a random integer up to that maximum, divByRand
as a whole is nondeterministic; we have control over the numerator, but not the denominator.
But that's where Testify Mock package comes in; we can make it so in a given test, we do get back the same value for rand.Intn
every time.
First let's install Testify Mock with:
go get github.com/stretchr/testify/mock
Now we're ready to use Testify Mock to give divByRand
some test coverage!
✨ Using Testify Mock
Now that we've got our nondeterministic code, here's the steps to making it testable with Testify Mock:
- Take the nondeterministic piece of functionality and wrap it in a Go interface type.
- Write an implementation of the interface that uses Testify Mock.
- In the tests, use the mock implementation to select deterministic results of the functions your interface calls.
Besides injecting your mock into your code, the test is otherwise written like any other Go test.
Let's try it out on divByRand
, step by step!
1. Take the nondeterministic function calls and wrap them in an interface
For divByRand
, the nondeterministic code we're working with is math/rand
's Intn
function, which takes in an integer and returns another integer. Here's what wrapping that method in an interface looks like:
type randNumberGenerator interface {
randomInt(max int) int
}
Now that we've got our interface, here's the plain implementation that calls the standard library's rand.Intn
.
// our standard-library implementation is an empty
// struct whose randomInt method calls math/rand.Intn
type standardRand struct{}
func (s standardRand) randomInt(max int) int {
return rand.Intn(max)
}
Now that we've got one interface implementation, we can use that in our divByRand
function like this:
func divByRand(
numerator int,
+ r randNumberGenerator,
) int {
- return numerator / rand.Intn(10)
+ return numerator / r.randomInt(10)
}
We now would call divByRandom in our production code using a function call like divByRandom(200, standardRand{})
.
In tests, though, we will instead use a mock implementation of our randNumberGenerator
interface, which we'll write in the next section.
2. Write an implementation of the interface that uses Testify Mock
The Testify Mock package's main type is Mock
, which handles the logic for mock API calls, such as:
- For each method being mocked, keeping track of how many times it was called, and with what arguments.
- Providing the coder with a way to specify return values to get back from the mock implementation when specified arguments are passed into given function calls.
We would first set up our mock implementation by embedding a Mock into a struct:
type mockRand struct { mock.Mock }
func newMockRand() *mockRand { return &mockRand{} }
By embedding a Mock
, now the mockRand
type has the methods for registering an API call that you expect to happen in the tests.
For example, at the beginning of a test that uses the randomNumberGenerator
interface, we could call:
var m mockRand
m.On("randomInt", 10).Return(6)
which says "if the number 10 is passed in as the maximum number for this mockRand
's randomInt
method, then always return the number 6".
In order to be able to use m.On("randomInt", arg)
however, we will need to actually give our mockRand
a randomInt
method. Here's how we would do that:
func (m *mockRand) randomInt(max int) int {
args := m.Called(max)
return args.Int(0)
}
We've got two lines of code, let's take a look at what they do. If it seems confusing at first, no worries; it will make more sense when we are actually using this code in a test.
- In the first line when
randomInt
is called, we call Mock.Called to record that it was called with the value passed in formax
. - Additionally,
m.Called
returns an Arguments object, which contains the return value(s) that we specified in the test to be returned if the function gets the given value ofmax
. For example, if we calledm.On("randomInt", 20).Return(7)
in the test,m.Called(20)
would return anArguments
object holding the return value 7. - Finally, to retrieve the return value, in the second line we call
args.Int(0)
, which means "return the zeroeth return value, and it will be of type int".
Note that in addition to Arguments.Int, there's also Bool, String, and Error methods, which give us back the n-th return value in those types. And if you have a return value of another type, you would retrieve it with the Arguments.Get method, which returns an interface{}
you would then convert to the type you expect to get back.
3. In the tests, use the mock implementation and feed in results the function returns.
We've got our mockRand
type implementing the randomNumberGenerator
interface, so it can be passed into divByRand
in Go code, including in our tests. The steps of our test now are:
- Create an instance of your mock interface implementation.
- Specify what results you want back when the mock's methods are called with a given set of arguments using the On and Return methods.
- Run the code that's being tested, the standard way you would in a Go test.
- Optionally, use methods like Mock.AssertCalled to check that a given method indeed had been called during the test.
Here's what that looks like in code:
package main
import (
"testing"
)
func TestDivByRand(t *testing.T) {
// get our mockRand
m := newMockRand()
// specify our return value. Since the code in divByRand
// passes 10 into randomInt, we pass 10 in as the argument
// to go with randomInt, and specify that we want the
// method to return 6.
m.On("randomInt", 10).Return(6)
// now run divByRand and assert that we got back the
// return value we expected, just like in a Go test that
// doesn't use Testify Mock.
quotient := divByRand(30, m)
if quotient != 5 {
t.Errorf("expected quotient to be 5, got %d", quotient)
}
// check that randomInt was called with the number 10;
// if not then the test fails
m.AssertCalled(t, "randomInt", 10)
}
Run go test -v
and you'll get a passing test. But there's a bug in the original implementation of divByRand
, so let's find it and give that bug some test coverage!
🐛 Finding and fixing a bug using Testify Mock
We wrote a test that utilizes Testify Mock, so now let's try using it to find a bug!
Since we're dividing by a nondeterministic value, we should have test coverage to make sure we can't accidentally divide by zero.
func TestDivByRandCantDivideByZero(t *testing.T) {
m := newMockRand()
m.On("randomInt", int64(10)).Return(int64(0))
quotient := divByRand(30, m)
if quotient != 30 {
t.Errorf("expected quotient to be 30, got %d", quotient)
}
}
This time around, when we pass 10 into our mockRand's randomInt method, we return 0. So in divByRand, we end up dividing by 0.
Run go test -v
and you should get:
--- FAIL: TestDivByRandCantDivideByZero (0.00s)
panic: runtime error: integer divide by zero [recovered]
panic: runtime error: integer divide by zero
A panic from dividing by zero. Let's fix divByRand
method to prevent this:
func divByRand(n int, r randNumberGenerator) int {
- return n / r.randomInt(10)
+ denominator := 1 + int(r.randomInt(10))
+ return n / denominator
}
Now that we're passing 9 instead of 10 into our call to randomInt
, we do need to update our test coverage to register a call to randomInt(9)
instead of randomInt(10)
, and for TestDivByRand, we need to return 5 instead of 6 since in divByRand we add 1 to the returned value.
func TestDivByRand(t *testing.T) {
var m mockRand
- m.On("randomInt", 10).Return(6)
+ m.On("randomInt", 10).Return(5)
Run go test -v
one more time and you should get...
=== RUN TestDivByRand
--- PASS: TestDivByRand (0.00s)
=== RUN TestDivByRandCantDivideByZero
--- PASS: TestDivByRandCantDivideByZero (0.00s)
PASS
ok github.com/andyhaskell/testify-mock 0.114s
Passing tests!
We've looked at how to use Testify Mock to write test coverage for randomness. In the next tutorial, we'll take a look at using Testify Mock to mock out a web API call.
Top comments (2)
ThankYou, Very nice explanation for this 2.Write an implementation of the interface that uses Testify Mock