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?

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.

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() or fact(). I also want to avoid abstract placeholder methods like foo() and bar(), 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 the m.isOddImpl() trampoline to make the override behavior possible in tests—although it would work to just call isOddHelper(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 requires mockgen from the gomock package

If you have feedback or experience with this style of testing, let’s continue the conversation on Twitter or Hacker News. Let me know if this helps you, or if you find a simpler way of doing this style of testing in Go.

Happy testing!


  1. Well, technically you can do it, but it’s not portable and very involved. We are looking for general, portable solutions in Go, not architecture-specific solutions in assembly. ↩︎