Unit testing in Go and its importance

Daryl Ng
NE Digital
Published in
3 min readFeb 16, 2021

I think we can all agree that we did not enjoy writing unit tests when we first started coding. We want to write for new features and maybe some occasional bug fixes.

Then comes the day when we need to refactor or make some improvements… and BOOM!

Unit testing in Go is easy and there is even a built-intesting package for writing unit tests.

// foobar.go
package foobar
func DoSomething() string {
// do something amazing here
}

To create a test file in Go, simply append _test to the corresponding file.

So let’s create a file called foobar_test.go which is the test file for foobar.go and a test function with Test prepended to the function name you are writing the unit test for.

// foobar_test.gofunc TestDoSomething(t *testing.T) {
}

Although no additional library is required, stretchr/testify lets you make assertions which makes your test readable and cleaner.

func TestDoSomething(t *testing.T) {
something := foobar.DoSomething()
assert.Equal(t, "expected", something, "some message")
}

Now all these makes sense, but how do you write unit tests when there are external calls?

Easy! Use interfaces!

Unit testing with Interfaces

// Naming is hard and I know this sounds weird... 
// But let's just follow convention here:
// https://golang.org/doc/effective_go.html#interface-names
type Foobarer interface {
Foobar(int) error
}
type HelloWorld struct {
...
}
func (hw *HelloWorld) Foobar() error {
// Makes external call (eg. HTTP or DB)
}

If we have a method called Foobar which makes an external call, you should not need to test the method again when you are writing a unit test for another method or function. This makes the test complicated and it is no longer a unit test.

You can keep it simple by mocking the response of Foobar using an interface.

Creating mocks like a pro

Instead of creating a new type which implements Foobarer . You can use stretchr/testify mock package which allows you to mock the responses for each of your unit tests.

You can use this library to automatically generate the mocks for your interfaces.

To test a function that calls the methodFoobar , you will need to create a testObj and mock the intended response.

// foobar.gofunc DoSomething(f Foobarer) {
...
}

Suppose now we have a function DoSomething that takes the interface Foobarer .

// foobar_test.gotype MockedFoobarer struct{
mock.Mock
}
func (m *MockedFoobarer) Foobar(n int) error {
args := m.Called(n)
return args.Error(0)
}
func TestDoSomething(t *testing.T) {
testObj := new(MockedFoobarer)
// mock the intended response and return nil
testObj.On("Foobar", 123).Return(nil)
// or you can do this to return error
// testObj.On("Foobar", 123).Return(errors.New("something happened"))
foobar.DoSomething(testObj) // asserts Foobar is called with 123 and returns nil
testObj.AssertExpectations(t)
}

In the above example, we are able to test DoSomething and assert that Foobar takes input 123 and returns nil .

Unit testing might be tedious, but it should not be difficult. Using these testing or mocking libraries help make the process a lot easier.

At NE Digital, we build technologies that impacts many. We are constantly improving our codebase and that is what makes unit testing important.

Our pull requests go through our CI/CD pipeline which automatically runs the unit tests we wrote, and these pull requests fail if it does not reach at least 80% code coverage.

Unit tests might require additional man hours but this can potentially reduce bugs or accidental breaking changes. And this in turn, save the many man hours required for fixing those bugs or changes and even operational costs.

Unit testing is important but it should not be the only type of testing you are doing. Integration testing, performance testing and functional testing, to name a few, plays a part in NE Digital to help create a reliable and robust system.

--

--