Gomock and Go Routines

Andrew Poydence
3 min readDec 12, 2018

--

When writing tests, gophers will often bump into the scenario where they have SUT (software under test) that use a go routine. Concurrency is always hard and there are countless articles on how to best write and test concurrent systems.

One area where this can become in particularly hard is when using mocks. gomock is no exception. When a test exits before the underlying code has time to schedule and execute the go routines, Controller.Finish() will fail the test. Below is a proposed pattern to help with this.

Consider the following SUT (foo.go):

package asyncimport "time"type Foo interface {
Bar()
}
func DoAsync(f Foo) {
go func() {
time.Sleep(100 * time.Millisecond)
f.Bar()
}()
}

Notice that the function DoAsync() uses the Foo object within a go routine.

NOTE: You can generate the mocks with the following:

mockgen \
--source=foo.go \
--destination=mock_test.go \
--package=async_test

Now lets consider some test code:

func TestDoAsyncWrong(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFoo := NewMockFoo(ctrl)
// Assert that Foo() is invoked
mockFoo.
EXPECT().
Bar()
async.DoAsync(mockFoo)
}

This fails…

go test
--- FAIL: TestDoAsyncWrong (0.00s)
foo_test.go:44: missing call(s) to *async_test.MockFoo.Bar() foo_test.go:41
foo_test.go:44: aborting test due to missing call(s)
FAIL
exit status 1
FAIL test/async 0.140s

But why? Our SUT clearly invokes Foo() . The problem is, the test finishes and the defer ctrl.Finish() fires before the go routine is scheduled. So how then do we deal with this?

We need a mechanism to ensure that the test doesn’t finish until we’re ready for it to. This is where sync.WaitGroup can be quite helpful:

func TestDoAsync(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
mockFoo := NewMockFoo(ctrl) // Assert that Foo() is invoked
mockFoo.
EXPECT().
Bar().
Do(func() {
wg.Done()
})
async.DoAsync(mockFoo)
}

We use the sync.WaitGroup to block the test until Bar() has been invoked. (NOTE: If we so desired, we could change the wg.Add(1) to wg.Add(5) if we instead wanted Bar() to be invoked 5 times) This will also work if the go routine is removed and the code becomes synchronous. This tends to be nice as it treats the SUT as more of a black box.

Conclusion

This pattern works because it forces the test to stay within scope until the mock has been used as expected. This has downsides as the test is further dictating what the SUT has to look like. However, there are cases where its nice to be able to do this and the before mentioned pattern will suit those.

A caveat to this is if the SUT is launching many go routines. Any go routine could cause a failure and you might notice a race. testing.T is not thread safe. This means that if several go routines make calls that the mock doesn’t want, then several go routines are also going to fail the test. This will make the race detector (if you ran with it) unhappy.

--

--

Andrew Poydence

#Cloud Developer at @Google. I am love with writing #Go, exploring #GCP and the cloud in general. Opinions stated here are my own.