DEV Community

Andy Haskell for Salesforce Engineering

Posted on

Mocks in Go tests with Testify Mock

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))
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Take the nondeterministic piece of functionality and wrap it in a Go interface type.
  2. Write an implementation of the interface that uses Testify Mock.
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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{} }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. In the first line when randomInt is called, we call Mock.Called to record that it was called with the value passed in for max.
  2. 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 of max. For example, if we called m.On("randomInt", 20).Return(7) in the test, m.Called(20) would return an Arguments object holding the return value 7.
  3. 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:

  1. Create an instance of your mock interface implementation.
  2. 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.
  3. Run the code that's being tested, the standard way you would in a Go test.
  4. 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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)