5 simple tips and tricks for writing unit tests in #golang

Mat Ryer
5 min readApr 30, 2015

Test-driven development is a great way to keep the quality of your code high, while protecting yourself from regression and proving to yourself and others that your code does what it is supposed to.

Here are five tips and tricks that can improve your tests.

1. Put your tests in a different package

Go insists that files in the same folder belong to the same package, that is except for `_test.go` files. Moving your test code out of the package allows you to write tests as though you were a real user of the package. You cannot fiddle around with the internals, instead you focus on the exposed interface and are always thinking about any noise that you might be adding to your API.

This frees you up to change the internals however you like without having to adjust your test code.

2. Internal tests go in a different file

If you do need to unit test some internals, create another file with `_internal_test.go` as the suffix. Internal tests will necessarily be more brittle than your interface tests — but they’re a great way to ensure internal components are behaving, and are especially useful if you do test-driven development.

3. Run all tests on save

Go builds and runs very quickly, so there’s little to no reason why you shouldn’t run your entire test suite every time you hit save.

While you’re at it, why not run go vet, golint and goimports at the same time?

In Sublime Text, this can be acheived by installing GoSublime and hitting `Cmd+.,5` before adding the following configuration items:

“on_save”: [{
“cmd”: “gs9o_run_many”, “args”: {
“commands”:[
[“clear”],
[“sh”, “if [ -f onsave.sh ]; then ./onsave.sh; else gofmt -s -w ./ && go build . errors && go test -i && go test && go vet && golint ; fi”]
],
“focus_view”: false
}
}],
“fmt_cmd”: [“goimports”]

The above script first checks to see if the project has an `onsave.sh` file, which it will run instead. This allows you to easily turn off the auto-test feature for packages where it is not appropriate.

4. Write table driven tests

Anonymous structs and composite literals allow us to write very clear and simple table tests without relying on any external package.

The following code allows us to setup a range of tests for an as-yet unwritten `Fib` function:

var fibTests = []struct {
n int // input
expected int // expected result
}{
{1, 1},
{2, 1},
{3, 2},
{4, 3},
{5, 5},
{6, 8},
{7, 13},
}

Then our test function just ranges over the slice, calling the `Fib` method for each `n`, before asserting that the results are correct:

func TestFib(t *testing.T) {
for _, tt := range fibTests {
actual := Fib(tt.n)
if actual != tt.expected {
t.Errorf("Fib(%d): expected %d, actual %d", tt.n, tt.expected, actual)
}
}
}

See if you can write the `Fib` function yourself to make the tests pass or you can get the answer from Dave Chaney.

5. Mock things using Go code

If you need to mock something that your code relies on in order to properly test it, chances are it is a good candidate for an interface. Even if you’re relying on an external package that you cannot change, your code can still take an interface that the external types will satisfy.

After a few years of writing mocks, I have finally found the perfect way of mocking interfaces, and I made a tool write the code for us without us having to add any dependencies to our project: Check out Moq.

Let’s say we’re importing this external package:

package mailmanimport “net/mail”type MailMan struct{}func (m *MailMan) Send(subject, body string, to ...*mail.Address) {
// some code
}
func New() *MailMan {
return &MailMan{}
}

If the code we’re testing takes a `MailMan` object, the only way our test code can call it is by providing an actual `MailMan` instance.

func SendWelcomeEmail(m *MailMan, to ...*mail.Address) {...}

This means that whenever we run our tests, a real email could be sent. Imagine if we’ve implemented the on save feature from above. We’d quickly annoy our test users or run up big service bills.

An alternative is to add this simple interface to your code:

type EmailSender interface{
Send(subject, body string, to ...*mail.Address)
}

Of course, the `MailMan` already satisfies this interface since we took the `Send` method signature from him in the first place — so we can still pass in `MailMan` objects as before.

But now we can write a test email sender:

type testEmailSender struct{
lastSubject string
lastBody string
lastTo []*mail.Address
}
// make sure it satisfies the interface
var _ package.EmailSender = (*testEmailSender)(nil)
func (t *testEmailSender) Send(subject, body string, to ...*mail.Address) {
t.lastSubject = subject
t.lastBody = body
t.lastTo = to
}

Now we can update our `SendWelcomeEmail` function to take the interface, rather than the concrete type:

func SendWelcomeEmail(m EmailSender, to ...*mail.Address) {...}

In our test code, we can send in our fake sender instead and make assertions on the fields after calling the target function:

func TestSendWelcomeEmail(t *testing.T) {
sender := &testEmailSender{}
SendWelcomeEmail(sender, to1, to2)
if sender.lastSubject != "Welcome" {
t.Error("Subject line was wrong")
}
if sender.To[0] != to1 && sender.To[1] != to2 {
t.Error("Wrong recipients")
}
}
  • If you want to explore mocking further, then be sure to check out the Moq tool — not only does it describe a great way of writing mocks, it also writes them for you.

Treat yourself and your brain: Go Programming Blueprints

Build real tools, apps and services while exploring good practices in Go.

Go Programming Blueprints: Second Edition

Buy now

--

--

Mat Ryer

Founder at MachineBox.io — Gopher, developer, speaker, author — BitBar app https://getbitbar.com — Author of Go Programming Blueprints