Writing testable Go code

Filed under golang on March 11, 2020

Testing in golang is much like any other language, and Go even comes with some of its own tools to write your tests with.

Today I’m going to share a couple of the tricks I’ve learned to make sure the code comes out smelling like a patch of roses with a sensible amount of code coverage

Build Tools

I always have some makefile targets to run my tests and build tags in my code to run all my tests.

Makefile target

If you’re like me and have trouble remembering all the intricacies of a command, it’s definitely worth having a target which will test everything for you

coverage: vendor
	mkdir -p reports || exit 0
	go test -v -tags unit -coverprofile=reports/cover.out ./...

This will run the tests tagged with unit and put the coverage report out to reports/cover.out. I tend to also have a variable to hold my mkdir command, since Windows does magical things if you run the wrong thing.

Build tags

One of the coolest things in the Go toolchain is build tags, where you can compile in different functionality based on a variable passed in at build time. To add these, simply add a line like this to the top of your files

// +build unit

This is then passed as a parameter to our test command

go test -v -tags unit ./...

Properly tagging your tests means that you split up your integration tests and run them when appropriate rather than having to run them in tandem with the unit tests, allowing you to fail as quickly as possible.

Coding conventions

I’ve seen a few schools of thought on this, but I definitely lay out my code in a particular way to encourage component reuse across my repository.

Interfaces

I tend to use interfaces, but an alternative I’ve seen going around is assigning functions to fields at runtime. I don’t think either is necessarily wrong, but I feel more comfortable using interfaces having come from the Java world.

The drawback to my preferred way of doing it is that it introduces the need to mock out interfaces where I use them. I generate these with Mockery, though I don’t think it’s in active development any more and I should probably find an alternative.

Pass state in function parameters

In circumstances where I don’t have structs, I will pass state in and avoid global variables completely. This means that I have predictable input to the function, and I can know what to expect when it returns.

Golang provides a nice construct called context to support this.

Smaller, more predictable functions

I’m of the opinion that code should be readable before it’s performant, and composable functions should be at the core of that.

Smaller functions are easier to test, so if it makes it easier to read the code I’ll generally refactor a little to pull it out.

No magic in web services

Something that always frustrated me about Spring when I was in the Java world was all the magic injections and wrappers around requests, to the point where it’s difficult to untangle and isolate functionality for testing.

Golang’s raw HTTP package is nice enough to give you the request and response objects and that’s it. Everything has to be wired up mostly manually, and this means you can test HTTP services quite easily. Use the httptest package to get some useful constructs like the httptest.Server httptest.ResponseRecorder.

There are some good examples on Golang.org about how to use the httptest package, which I’ll reproduce here

package main

// httptest.Server example
import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
)

func main() {
	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello, %s", r.Proto)
	}))
	ts.EnableHTTP2 = true
	ts.StartTLS()
	defer ts.Close()

	res, err := ts.Client().Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	greeting, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s", greeting)

}
package main

// httptest.ResponseRecorder example
import (
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "<html><body>Hello World!</body></html>")
	}

	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	w := httptest.NewRecorder()
	handler(w, req)

	resp := w.Result()
	body, _ := ioutil.ReadAll(resp.Body)

	fmt.Println(resp.StatusCode)
	fmt.Println(resp.Header.Get("Content-Type"))
	fmt.Println(string(body))

}

And of course, my most important tip…

Test Driven Development

Writing tests first is a sure fire way to make sure you get them done and design your code in such a way to support it. I usually achieve this by creating a function that calls panic, then writing my test cases, and finally implementing the function.

It’s something I’ve been trying to win my team around to, but I think it’s going to take a while to convince them, and it does feel completely ass backwards when you first try it. I can guarantee your testing coverage will go up and you’ll find yourself giving more thought to the overall structure of your code.


Stephen Gream

Written by Stephen Gream who lives and works in Melbourne, Australia. You should follow him on Minds