A pattern for Go tests

I used to spend an unreasonable amount of time thinking about how to begin writing a test.

I googled test patterns in Go.

Many people seem to rely on external dependencies for assertions. And in fact, I understand that generic (aha!) functions like isNil(v interface{}) bool can initially bring some speed to the development. But in the long run, I think that embracing the true strongly-typed nature of Go, instead of just searching for a way around it, is more rewarding. Writing more idiomatic code will be beneficial both for the quality of the code, and for the insights you can get by looking the Beast in the eye.

Then, I invoked the Spirits of the Core Library.

As soon as I realised where to look at, I indeed saw a sign.

Deep down in the Go core library, there is a package that was specifically written for testing purposes: net/http/httptest. This one had to have good tests.

What I found

Brad Fitzpatrick’s code, what else.

Here is a slightly adapted version of recorder_test.go:

What is going on?

The test function has three parts.

The first part (lines 2–28): the matchers. The first line defines a function type: checkFunc. This function signature has an argument for every value we will ever want to test. The arguments of checkFunc should include all the return values of the target function. In this case, since we are testing the methods of a ResponseRecorder, the state we want to test is in the ResponseRecorder itself. This will be the only argument of the checkFunc.

The matcher functions are closures: provided with the expected value, the returned checkFunc will error if the expectation is not matched.

The second part (lines 30–60): the test cases. An anonymous struct is holding the test data. Every test is defined with:

  • the description of the case
  • the input
  • a slice of checkFunc carrying the expectations.

The third part (lines 62–74): the testing logic. Here is where we get our hands dirty, using the data from the test cases to prepare and execute the actual target code. Then we range over the checkFunc slice: if an error is returned, it can directly be passed to t.Error().

TDD or BDD, the choice is yours

If you are more into table-driven tests, the matchers will probably be repeated in every test with different values, and the name of the test case will describe a situation in which you want the target logic to function.

For a BDD dev, the test case name will be the description of the expected outcome, and not all the matchers will likely be used in every case.

If instead, like me, you just want your CLI to be flooded with tests, you can mix and match until you’re happy!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.