OO-style testing in Go (2024)

OO-style testing in Go (1)

Testing in OO languages like C++ or Java is often done with mocks: using aninterface (create a new one if you don’t already have it), create a mock, setup 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 classto work as-is, but just override a specific class method to be a mock withexpectations, rathe than the full implementation, to test specific cases.

In dynamic languages like Python and JavaScript, it’s even easier: you can justoverride 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, norare we trying to recreate features of Java or C++ in Go. In general, we shouldavoid reimplementing features from other languages which are not idiomatic toanother language, but in this case, it was the best approach we found at thetime.

This use case which necessitated a way to mock out some intermediate methodsinvolved data which was hierarchical in nature; that is, it would processentities of type A which could contain some entities of type B, which inturn may contain entities of type C.

We have an object with number of methods which are calling other methods on thesame object—ProcessA(), ProcessB(), and ProcessC() in the diagram below areprocessing entities of type A, B, and C, respectively—and each of thesemethods end up calling some methods (M1, M2, M3) on an external service:

OO-style testing in Go (2)

Of course, we have already created mocks for the external service E using itsinterface; however, if we leave it at that, anytime we want to test thebehavior of A, we have to include all the side-effects of all of its downstreamuses of B and C.

This means a lot of repetition, because if we already have tests for C, we willalso have to include some of them in the tests for B, and then we will alsohave to include some of C and B side effects in each of the tests for A, whichcreates 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 callExt.M3()
  • for a test involving ProcessB(), we can set expectations that it will callProcessC() and/or and Ext.M2(), depending on the test case
  • for a test involving ProcessA(), we can set expectations that it will callProcessB() 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 ofwhat downstream effects will happen by utilizing their calls through otherintermediate methods. Alternatively, if we have to end up specifying all of thefinal effects on E for each of the high-level test cases, we will create verylong, duplicated, and brittle tests, because we will have to update tests forA, B, and C every time something changes in C, even though no code changed in Aor B, for example.

Alternatively, we could split each of A, B, and C into separate structs, andinsert an interface between each pair of them, leading to a more complexarchitecture, since each of A, B, and C would now be separate methods ofdifferent objects, whereas before, they were methods of a single object, so wewould also have to duplicate state:

OO-style testing in Go (3)

In an object-oriented language such as Java or C++, we would just end upmocking 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-styleinheritance and overrides?

A straight-forward attempt with mocks

While Go has interfaces, it doesn’t have OO-style class inheritance, so youcan’t just subclass an existing implementation and override a method.

Note: Go provides the functionality of embedding the implementation of onestruct in another, where you can technically override a method. However, sinceGo only has static dispatch, and not dynamic dispatch, if we use this approachto override the B or C methods above, we will find that calling the A methodthat it will still call the original methods B and C, and not the ones weprovided. For more details, see the appendix.

Additionally, you can’t simply change which implementation a specific methodname points to easily1, or can we?

Let’s recall that OO languages typicaly implement their objects by usingvtables, which is basically a struct of function pointers.

The way we generally implement a Go interface, however, does not have anexplicit struct of function pointers. For example, consider a very simpleinterface:

type MyInterface interface {IsOdd(n uint) boolIsEven(n uint) bool}

The way we would implement it is by simply declaring functions of that typewith an arbitrary struct that holds our state (if any—it could also beempty), 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 todemonstrate methods that depend on each other, rather than just independent,stand-alone methods like fib() or fact(). I also want to avoid abstractplaceholder methods like foo() and bar(), so we have something concreteto discuss.

Also, consider that these methods may have side-effects, or issue RPCs, or dosome other heavy-weight work that we might want to avoid in some cases intests.

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/oo1package oo1type MyInterface interface {IsOdd(n uint) boolIsEven(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 oo1import ("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):

OO-style testing in Go (4)

Now, let’s say we want to mock out one of the methods, while using the realimplementation of the other method—how could we do that? In an OOlanguage, we could just inherit from the base implementation and override themethod we want to mock, but in Go, when we use mockgen to generatea 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 compilefunc 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&mldr; it looks like our struct MyImpl does have function pointers, butthey’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 morelevel 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, solet’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 anobject that implements the original interface, so that we can call all of theother 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) boolisEvenHelper(m MyInterface, n uint) bool}

Unlike the public methods IsOdd() and IsEven(), these method names beginwith a lowercase letter, which means they’re not exposed outside of thepackage, which is fine, since they’re only for testing, and our test can accessthese methods in order to be able to mock them. Outside of the package, userswill not be able to address them, which is great, since this is just animplementation detail to enable testing.

Additionally, note that the type of the first object is the interface, notthe concrete struct that is the implementation. We’re essentially using thePython style of an explicit first parameter being self so that we can patchan object with a function; since the function is stand-alone, it doesn’t havean automatic equivalent of a this or self reference in scope, so we have toprovide it explicitly.

Next, let’s extend our state object to include mutable fields of the same typesas the functions in the interface:

type MyImpl struct {isOddHelper func(m MyInterface, n uint) boolisEvenHelper 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 thenew 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 intests—although it would work to just call isOddHelper(m, n) here, itwouldn’t help us, as it would statically bind the interface to theimplementation, 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 = isOddHelperImplm.isEvenImpl = isEvenHelperImplreturn m}

This function needs to return *MyImpl rather than simply MyInterfacebecause we need to be able to reassign the fields isOddImpl and IsEvenImplin 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.isOddHelpermock.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):

OO-style testing in Go (5)

As you can see, in prod, MyImpl.IsEven() forwards the call throughMyImpl.isEvenImpl (func-typed field), which is set by default to the static implementation bythe NewMyImpl constructor. In a test, we can set MyImpl.isEvenImpl topoint to the mock version of isEvenHelper(), and set expectations on isEvenHelper().

Here’s a more concrete diagram with how the different objects are wired inprod vs. test modes:

OO-style testing in Go (6)

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/oo2package oo2type MyInterface interface {IsOdd(n uint) boolIsEven(n uint) bool}type MyInterfaceHelper interface {isOddHelper(m MyInterface, n uint) boolisEvenHelper(m MyInterface, n uint) bool}type MyImpl struct {isOddImpl func(m MyInterface, n uint) boolisEvenImpl func(m MyInterface, n uint) bool}func NewMyImpl() *MyImpl {m := new(MyImpl)m.isOddImpl = isOddHelperImplm.isEvenImpl = isEvenHelperImplreturn 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 oo2import ("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.isOddHelpermock.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 thegomock package

If you have feedback or experience with this style of testing, let’s continuethe conversation on Twitter or Reddit. Let me know if thishelps 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 onestruct in another, where you can technically override a method. However, sinceGo only has static dispatch, and not dynamic dispatch, if we use this approachto override the B or C methods above, we will find that calling the A methodthat it will still call the original methods B and C, and not the ones weprovided.

Here’s a complete example:

package maintype 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 returnfalse, 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.

  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.↩︎

OO-style testing in Go (2024)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Kerri Lueilwitz

Last Updated:

Views: 6243

Rating: 4.7 / 5 (67 voted)

Reviews: 90% of readers found this page helpful

Author information

Name: Kerri Lueilwitz

Birthday: 1992-10-31

Address: Suite 878 3699 Chantelle Roads, Colebury, NC 68599

Phone: +6111989609516

Job: Chief Farming Manager

Hobby: Mycology, Stone skipping, Dowsing, Whittling, Taxidermy, Sand art, Roller skating

Introduction: My name is Kerri Lueilwitz, I am a courageous, gentle, quaint, thankful, outstanding, brave, vast person who loves writing and wants to share my knowledge and understanding with you.