Testing in OO languages like C++ or Java is often done with mocks: using an interface (create a new one if you don’t already have it), create a mock, set up some expected calls and return values or side-effects, and you’re all set.
Or, you can subclass an existing implementation when you want the entire class to work as-is, but just override a specific class method to be a mock with expectations, rathe than the full implementation, to test specific cases.
In dynamic languages like Python and JavaScript, it’s even easier: you can just override fields or even entire methods to do whatever you want them to do.
So how do we do this in Go?
Motivation
First, why do we even want to do this? This is not merely idle curiosity, nor are we trying to recreate features of Java or C++ in Go. In general, we should avoid reimplementing features from other languages which are not idiomatic to another language, but in this case, it was the best approach we found at the time.
This use case which necessitated a way to mock out some intermediate methods involved data which was hierarchical in nature; that is, it would process entities of type A which could contain some entities of type B, which in turn may contain entities of type C.
We have an object with number of methods which are calling other methods on the same object—ProcessA(), ProcessB(), and ProcessC() in the diagram below are processing entities of type A, B, and C, respectively—and each of these methods end up calling some methods (M1, M2, M3) on an external service:
Of course, we have already created mocks for the external service E using its interface; however, if we leave it at that, anytime we want to test the behavior of A, we have to include all the side-effects of all of its downstream uses of B and C.
This means a lot of repetition, because if we already have tests for C, we will also have to include some of them in the tests for B, and then we will also have to include some of C and B side effects in each of the tests for A, which creates a lot of code duplication and makes the test code much harder to read.
Ideally, we would test each method in isolation, that is:
- for a test involving ProcessC(), we can set expectations that it will call Ext.M3()
- for a test involving ProcessB(), we can set expectations that it will call ProcessC() and/or and Ext.M2(), depending on the test case
- for a test involving ProcessA(), we can set expectations that it will call ProcessB() and/or ProcessC() and/or Ext.M1(), depending on the test case
Note that in each of the cases for B and A, we can abstract the knowledge of what downstream effects will happen by utilizing their calls through other intermediate methods. Alternatively, if we have to end up specifying all of the final effects on E for each of the high-level test cases, we will create very long, duplicated, and brittle tests, because we will have to update tests for A, B, and C every time something changes in C, even though no code changed in A or B, for example.
Alternatively, we could split each of A, B, and C into separate structs, and insert an interface between each pair of them, leading to a more complex architecture, since each of A, B, and C would now be separate methods of different objects, whereas before, they were methods of a single object, so we would also have to duplicate state:
In an object-oriented language such as Java or C++, we would just end up mocking out one or more of the methods in our class and set expectations on it, to avoid having to duplicate code into each test case.
How could we solve the same issue in Go, which doesn’t have OO-style inheritance and overrides?
A straight-forward attempt with mocks
While Go has interfaces, it doesn’t have OO-style class inheritance, so you can’t just subclass an existing implementation and override a method.
Note: Go provides the functionality of embedding the implementation of one struct in another, where you can technically override a method. However, since Go only has static dispatch, and not dynamic dispatch, if we use this approach to override the B or C methods above, we will find that calling the A method that it will still call the original methods B and C, and not the ones we provided. For more details, see the appendix.
Additionally, you can’t simply change which implementation a specific method name points to easily1, or can we?
Let’s recall that OO languages typicaly implement their objects by using vtables, which is basically a struct of function pointers.
The way we generally implement a Go interface, however, does not have an explicit struct of function pointers. For example, consider a very simple interface:
type MyInterface interface {
IsOdd(n uint) bool
IsEven(n uint) bool
}
The way we would implement it is by simply declaring functions of that type with an arbitrary struct that holds our state (if any—it could also be empty), e.g.:
type MyImpl struct {}
func (m *MyImpl) IsOdd(n uint) bool {
if n == 0 {
return false
} else if n == 1 {
return true
}
return m.IsEven(n - 1)
}
func (m *MyImpl) IsEven(n uint) bool {
if n == 0 {
return true
} else if n == 1 {
return false
}
return m.IsOdd(n - 1)
}
Note: I’m using a pair of mutually-recursive methods because I want to demonstrate methods that depend on each other, rather than just independent, stand-alone methods like
fib()
orfact()
. I also want to avoid abstract placeholder methods likefoo()
andbar()
, so we have something concrete to discuss.Also, consider that these methods may have side-effects, or issue RPCs, or do some other heavy-weight work that we might want to avoid in some cases in tests.
And let’s write some tests for this code:
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOdd(t *testing.T) {
m := new(MyImpl)
assert.True(t, m.isOdd(35))
assert.False(t, m.isOdd(64))
}
func TestEven(t *testing.T) {
m := new(MyImpl)
assert.False(t, m.isEven(35))
assert.True(t, m.isEven(64))
}
OK, that was easy.
Here’s the complete code we have so far:
Our implementation code (v1).
// Copyright 2020 Misha Brukman
// SPDX-License-Identifier: Apache-2.0
// https://misha.brukman.net/blog/2020/03/oo-style-testing-in-go/
// Note: the `-self_package` value here is synthetic; there isn't actually a
// Git repo at that URL. Feel free to change this to anything you want, but be
// sure to run `go mod init [...path..]` with that same path.
//
// In my case, I actually have `go.mod` and `go.sum` one level higher than this
// package:
//
// workdir
// ├── go.mod
// ├── go.sum
// ├── oo1/
// | ├── oo.go <-- this file
// | └── oo_test.go
// └── oo2/
//
// Thus, I ran:
//
// $ cd workdir
// $ go mod init gitlab.com/mbrukman/oo-testing-in-go
//
//go:generate mockgen -source oo.go -destination mock_oo.go -package oo1 -self_package gitlab.com/mbrukman/oo-testing-in-go/oo1
package oo1
type MyInterface interface {
IsOdd(n uint) bool
IsEven(n uint) bool
}
type MyImpl struct{}
func (m *MyImpl) IsOdd(n uint) bool {
if n == 0 {
return false
} else if n == 1 {
return true
}
return m.IsEven(n - 1)
}
func (m *MyImpl) IsEven(n uint) bool {
if n == 0 {
return true
} else if n == 1 {
return false
}
return m.IsOdd(n - 1)
}
Our test code (v1).
// Copyright 2020 Misha Brukman
// SPDX-License-Identifier: Apache-2.0
// https://misha.brukman.net/blog/2020/03/oo-style-testing-in-go/
package oo1
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestOdd(t *testing.T) {
m := new(MyImpl)
assert.True(t, m.IsOdd(35))
assert.False(t, m.IsOdd(64))
}
func TestEven(t *testing.T) {
m := new(MyImpl)
assert.False(t, m.IsEven(35))
assert.True(t, m.IsEven(64))
}
Here’s the diagram of the implementation (solid edges) and usage (dashed edges):
Now, let’s say we want to mock out one of the methods, while using the real
implementation of the other method—how could we do that? In an OO
language, we could just inherit from the base implementation and override the
method we want to mock, but in Go, when we use mockgen
to generate
a mock from an interface
, we get a mock of all the methods, not just one.
How do we keep one real method and one mock method?
Turns out, we can’t just assign one of the mock methods to the struct:
// Note: this might look fine, but it won't compile
func TestOverride(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockMyInterface(ctrl)
m := new(MyImpl)
m.IsOdd = mock.IsOdd
// Now we can set some expectations on m.IsOdd ... right?
}
However, if you try to compile this, you’ll get an error:
cannot assign to m.IsOdd
That’s odd… it looks like our struct MyImpl
does have function pointers, but
they’re all read-only! How can we fix this?
Fixing the method override
As we know, “every problem in computer science can be solved with just one more level of indirection”.
While we can’t modify a statically-defined method bound to a specific object,
recall that struct fields are mutable, even if they are of type func
, so
let’s create our own mutable struct fields so that we can reassign them!
However, note that stand-alone functions still need to have a pointer to an object that implements the original interface, so that we can call all of the other methods that are needed to implement the API.
First, let’s define a private helper interface for the new methods:
type MyInterfaceHelper interface {
isOddHelper(m MyInterface, n uint) bool
isEvenHelper(m MyInterface, n uint) bool
}
Unlike the public methods IsOdd()
and IsEven()
, these method names begin
with a lowercase letter, which means they’re not exposed outside of the
package, which is fine, since they’re only for testing, and our test can access
these methods in order to be able to mock them. Outside of the package, users
will not be able to address them, which is great, since this is just an
implementation detail to enable testing.
Additionally, note that the type of the first object is the interface
, not
the concrete struct
that is the implementation. We’re essentially using the
Python style of an explicit first parameter being self
so that we can patch
an object with a function; since the function is stand-alone, it doesn’t have
an automatic equivalent of a this
or self
reference in scope, so we have to
provide it explicitly.
Next, let’s extend our state object to include mutable fields of the same types as the functions in the interface:
type MyImpl struct {
isOddHelper func(m MyInterface, n uint) bool
isEvenHelper func(m MyInterface, n uint) bool
}
And we’ve renamed and re-implemented the original functions as stand-alone. Also, note that the argument is of the public interface type, not the struct:
func isOddHelperImpl(m MyInterface, n uint) bool {
if n == 0 {
return false
} else if n == 1 {
return true
}
return m.IsEven(n - 1)
}
func isEvenHelperImpl(m MyInterface, n uint) bool {
if n == 0 {
return true
} else if n == 1 {
return false
}
return m.IsOdd(n - 1)
}
We now need to reimplement the original MyInterface
interface in terms of the
new methods:
func (m *MyImpl) IsOdd(n uint) bool {
return m.isOddImpl(m, n)
}
func (m *MyImpl) IsEven(n uint) bool {
return m.isEvenImpl(m, n)
}
Note that the key here is for the
m.IsOdd()
call to go through them.isOddImpl()
trampoline to make the override behavior possible in tests—although it would work to just callisOddHelper(m, n)
here, it wouldn’t help us, as it would statically bind the interface to the implementation, preventing the override in tests, which is what we’re after.Naturally, the same applies to
m.IsEven()
as well.
What’s left is to remember that we can no longer use the simple new(MyImpl)
to create a new instance, because we need to initialize the new struct fields,
or we will cause a segfault at runtime, so let’s create a constructor:
func NewMyImpl() *MyImpl {
m := new(MyImpl)
m.isOddImpl = isOddHelperImpl
m.isEvenImpl = isEvenHelperImpl
return m
}
This function needs to return *MyImpl
rather than simply MyInterface
because we need to be able to reassign the fields isOddImpl
and IsEvenImpl
in tests, but these are only defined in the MyImpl struct
and nowhere else.
And here’s how we can now write a test for this code:
func TestOverride(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockMyInterfaceHelper(ctrl)
m := NewMyImpl()
m.isOddImpl = mock.isOddHelper
mock.EXPECT().isOddHelper(m, uint(34)).Return(false)
mock.EXPECT().isOddHelper(m, uint(63)).Return(true)
assert.False(t, m.IsEven(35))
assert.True(t, m.IsEven(64))
}
And there you have it!
Here’s the diagram of the new implementation (solid edges) and usage (dashed edges):
As you can see, in prod, MyImpl.IsEven()
forwards the call through
MyImpl.isEvenImpl
(func-typed field), which is set by default to the static implementation by
the NewMyImpl
constructor. In a test, we can set MyImpl.isEvenImpl
to
point to the mock version of isEvenHelper()
, and set expectations on isEvenHelper()
.
Here’s a more concrete diagram with how the different objects are wired in prod vs. test modes:
Finally, here are the complete source and test files:
Our implementation code (v2).
// Copyright 2020 Misha Brukman
// SPDX-License-Identifier: Apache-2.0
// https://misha.brukman.net/blog/2020/03/oo-style-testing-in-go/
// Note: the `-self_package` value here is synthetic; there isn't actually a
// Git repo at that URL. Feel free to change this to anything you want, but be
// sure to run `go mod init [...path..]` with that same path.
//
// In my case, I actually have `go.mod` and `go.sum` one level higher than this
// package:
//
// workdir
// ├── go.mod
// ├── go.sum
// ├── oo1/
// └── oo2/
// ├── oo.go <-- this file
// └── oo_test.go
//
// Thus, I ran:
//
// $ cd workdir
// $ go mod init gitlab.com/mbrukman/oo-testing-in-go
//
//go:generate mockgen -source oo.go -destination mock_oo.go -package oo2 -self_package gitlab.com/mbrukman/oo-testing-in-go/oo2
package oo2
type MyInterface interface {
IsOdd(n uint) bool
IsEven(n uint) bool
}
type MyInterfaceHelper interface {
isOddHelper(m MyInterface, n uint) bool
isEvenHelper(m MyInterface, n uint) bool
}
type MyImpl struct {
isOddImpl func(m MyInterface, n uint) bool
isEvenImpl func(m MyInterface, n uint) bool
}
func NewMyImpl() *MyImpl {
m := new(MyImpl)
m.isOddImpl = isOddHelperImpl
m.isEvenImpl = isEvenHelperImpl
return m
}
func (m *MyImpl) IsOdd(n uint) bool {
return m.isOddImpl(m, n)
}
func (m *MyImpl) IsEven(n uint) bool {
return m.isEvenImpl(m, n)
}
func isOddHelperImpl(m MyInterface, n uint) bool {
if n == 0 {
return false
} else if n == 1 {
return true
}
return m.IsEven(n - 1)
}
func isEvenHelperImpl(m MyInterface, n uint) bool {
if n == 0 {
return true
} else if n == 1 {
return false
}
return m.IsOdd(n - 1)
}
Our test code (v2).
// Copyright 2020 Misha Brukman
// SPDX-License-Identifier: Apache-2.0
// https://misha.brukman.net/blog/2020/03/oo-style-testing-in-go/
package oo2
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestOverride(t *testing.T) {
ctrl := gomock.NewController(t)
mock := NewMockMyInterfaceHelper(ctrl)
m := NewMyImpl()
m.isOddImpl = mock.isOddHelper
mock.EXPECT().isOddHelper(m, uint(34)).Return(false)
mock.EXPECT().isOddHelper(m, uint(63)).Return(true)
assert.False(t, m.IsEven(35))
assert.True(t, m.IsEven(64))
}
Notes:
- mocks are generated via the
go generate ./...
command - the
go generate
command requiresmockgen
from thegomock
package
If you have feedback or experience with this style of testing, let’s continue the conversation on Twitter or Reddit. Let me know if this helps you, or if you find a simpler way of doing this style of testing in Go.
Happy testing!
Appendix: embedding
Earlier, we mentioned that:
Note: Go provides the functionality of embedding the implementation of one struct in another, where you can technically override a method. However, since Go only has static dispatch, and not dynamic dispatch, if we use this approach to override the B or C methods above, we will find that calling the A method that it will still call the original methods B and C, and not the ones we provided.
Here’s a complete example:
package main
type T struct{}
func (t *T) Foo() bool {
print("t.Foo()\n")
return t.Bar()
}
func (t *T) Bar() bool {
print("t.Bar()\n")
return true
}
type U struct {
T
}
func (u *U) Bar() bool {
print("u.Bar()\n")
return false
}
func main() {
u := new(U)
print("calling u.Foo():\n")
ret := u.Foo()
print("ret: ", ret, "\n")
}
Running this code outputs the following:
calling u.Foo():
t.Foo()
t.Bar()
ret: true
If Go supported dynamic dispatch, we would expect u.Foo()
to call u.Bar()
,
but it ends up calling t.Bar()
instead. Also, we would expect it to return
false
, since that’s what the overriden method does, but it returns true
,
since that’s what the original method did.
Thus, we cannot use struct embedding to address our use case.