Write Unit Test in Go
Motivation
Better than error-driven
Recently I joined a company as IT Architect. The project I am involved is to evolve, design and implement a service written in Go which will run 24*7. In the previous version, there is zero test coverage. I believe people back then just deploy it and manually interact with it to see if it smokes, as one of mine colleague described, “it is error-driven”. I don’t feel well with this.
Test-driven learning
As a new joiner, despite of clarifications from my excellent and enthusiastic colleagues, there are still some blurry parts in our domain model and design choices confuse me a lot. To conquer this in an efficient way, I decide to write tests and involved experienced colleagues to review those tests so that the business logic in the service will be crystal clear to me. The following diagram illustrates this process:

I call this test-driven learning. And my understanding of the domain model is actually a side-effect of this process. The output is tests which has actual domain model and design decisions built-in. It’s different than code since it can also contain negative test cases to illustrate which expectations of business model and design choices are not true.
To write test in Go effectively, I’ve learned a lot from different materials and summarise into a “Unit Test in Go” workshop to transfer the knowledge to the team. This article is based on that.
PS: In one of these conferences, the presenter introduced himself as a person who like to sleep well, and claimed this is the reason he write test. I cannot agree more.
Code examples in this article can be found here.
Unit test
Here is a quick refresher about unit test: The form of unit test is a function that tests a specific piece or set of code from a package or program. And its target is to determine whether the code in question is working as expected for a given scenario.
Test the Go way
The go toolchain contains go test
, and testing
is part of Go’s standard library, which suggests Go community has its own opinionated way to conduct test.
Write test like other code
Simplicity is a core value of Go. In Go it is preferred to write test just like write other Go code. Meaning, stick with standard testing
package. And not write test in a “foreign language” (or DSL, Domain specific language).
A related point is that testing frameworks tend to develop into mini-languages of their own, with conditionals and controls and printing mechanisms, but Go already has all those capabilities; why recreate them? We’d rather write tests in Go; it’s one fewer language to learn and the approach keeps the tests straightforward and easy to understand.
Let’s see this via an example:
We can execute go test
to run the tests:
$ go test--- FAIL: TestAbsWrong (0.00s)
basic_test.go:23: AbsWrong(-1) = -1; want 1
FAIL
exit status 1
FAIL github.com/hughluo/go-unit-test/basic 0.264s
, whereas go test -v
is more verbose:
$ go test -v=== RUN TestAbs
--- PASS: TestAbs (0.00s)
=== RUN TestAbsWrong
basic_test.go:23: AbsWrong(-1) = -1; want 1
--- FAIL: TestAbsWrong (0.00s)
FAIL
exit status 1
FAIL github.com/hughluo/go-unit-test/basic 0.063s
Notice that we write the log in a way that looks like function calling syntax, this is also recommended to make the log easier to understand.
Here is how a function is identified as test:
Package testing provides support for automated testing of Go packages. It is intended to be used in concert with the “go test” command, which automates execution of any function of the form:
func TestXxx(*testing.T)
where Xxx does not start with a lowercase letter. The function name serves to identify the test routine.
To write a new test suite, create a file whose name ends _test.go that contains the TestXxx functions as described here. Put the file in the same package as the one being tested. The file will be excluded from regular package builds but will be included when the “go test” command is run.
Proper error handling, also in test
Notice that in the previous example, Line 13 and Line 14, we compare if the result equals to the value we want. If not, we use Errorf
to mark the test as FAIL
and print log about it. A function (test) marked as FAIL
does not influence other functions (tests).
Also if a test function is marked as FAIL
, the execution of the function does not stop. This is quite useful for writing subtests, which we will see when we write table-driven test in next section.
Proper error handling means letting other tests run after one has failed, so that the person debugging the failure gets a complete picture of what is wrong.
Still, we may want to stop the test function when something critical happened. For instance, it should stop executing if it failed to do some initiation in the test like creating a mock database client.
To do this, we will take a look into the methods for Type T (which is the type passed to Test functions to manage test state and support formatted test logs).
Here is a self-explaining example to illustrate some useful methods:
$ go test
fmt.Printf: do not use me in test...
--- FAIL: TestOutput (0.00s)
output_test.go:9: t.Logf: log in test
output_test.go:10: t.Errorf: Fail but continue to execute
output_test.go:12: t.Fatalf: stop executing this function!
FAIL
exit status 1
FAIL github.com/hughluo/go-unit-test/output 0.499s$ go test -v
=== RUN TestOutput
output_test.go:9: t.Logf: log in test
output_test.go:10: t.Errorf: Fail but continue to execute
fmt.Printf: do not use me in test...
output_test.go:12: t.Fatalf: stop executing this function!
--- FAIL: TestOutput (0.00s)
=== RUN TestOther
output_test.go:17: I am TestOther and will be executed, whether the last test failed or not
--- PASS: TestOther (0.00s)
FAIL
exit status 1
Write table-driven test
Table-driven test is an approach that defines test cases as a list of structs, and run each of those test cases. It is often combined with subtests via Run
method.
If the amount of extra code required to write good errors seems repetitive and overwhelming, the test might work better if table-driven, iterating over a list of inputs and outputs defined in a data structure.
The work to write a good test and good error messages will then be amortized over many test cases.
— Go FAQ: Where is my favorite helper function for testing in Go?
Let’s see how to write table-driven test via an example:
Notice that between Line 10 and Line 18, we define a list of anonymous structs as test cases. The struct then contains name
to describe the test case, a
and b
are input, whereas want
defines the expected output.
I would argue this approach has significant gain in readability and maintainability.
In Line 20, we use subtests via Run
method, which is the best friend of table-drive test. It takes two arguments: a string as test case name and a function as subtest.
The Run methods of T (…) allow defining subtests (…), without having to define separate functions for each. This enables uses like table-driven (…) and creating hierarchical tests. It also provides a way to share common setup and tear-down code (…)
And here is the hierarchical output:
$ go test -v=== RUN TestMultiply
=== RUN TestMultiply/b_is_zero
=== RUN TestMultiply/two_negative_numbers
--- PASS: TestMultiply (0.00s)
--- PASS: TestMultiply/b_is_zero (0.00s)
--- PASS: TestMultiply/two_negative_numbers (0.00s)
PASS
ok github.com/hughluo/go-unit-test/table 0.385s
More test examples
It is always good to learn from the standard library:
The standard Go library is full of illustrative examples, such as in the formatting tests for the
fmt
package.— Go FAQ: Where is my favorite helper function for testing in Go FAQ
Comparison via go-cmp
To compare values in test, it is recommended to use package go-cmp
developed within Google.
This package is intended to be a more powerful and safer alternative to
reflect.DeepEqual
for comparing whether two values are semantically equal.
See the documentation for more information.
Test Coverage
There is much more go test
toolchain can do for you, my favourite one is test coverage.
$ go test -coverprofile=coverage.outPASS
coverage: 66.7% of statements
ok github.com/hughluo/go-unit-test/basic 0.587s$ go tool cover -html=coverage.out
Above command will open your browser and show you something like this:

We can see that we forget to test against non-negative input for Abs
.
For more about test coverage, see The Go Blog: The cover story.
Conclusion
With this article, I believe I shall convey you that writing unit test in Go is straightforward and convenient. To sleep well, Let’s write more unit test and write them well!
Also keep this in mind:
Program testing can be used to show the presence of bugs, but never to show their absence!
—Dijkstra (1970) "Notes On Structured Programming" (EWD249), Section 3 ("On The Reliability of Mechanisms"), corollary at the end.
Thank you for reading :)
Reference
The Go Programming Language by Alan A. A. Donovan and Brian Kernighan, the authoritative book about Golang, though not so much about testing.
Go in Action by William Kennedy with Brian Ketelsen and Erik St. Martin, also a great book to learn Go.
Go in Practice by Matt Butcher and Matt Farina, focus on different aspect of applications via Go.
Test Driven by Lasse Koskela, a book about test. In the original workshop, I also touched TDD a bit, I may write another article about our practice on this topic later.
Gocon Canada 2019: Intro to Test-Driven Development in Go by Denise Yu, what a brilliant talk! I really like her comics and use those A LOT in my workshop.
GopherCon Denver 2017: Advanced Testing with Go by Mitchell Hashimoto, very informative, I learned a lot of techniques and practices from this one. As founder of Hashicorp, I didn’t expect he would dig deep into a topic like this. But yeah, he is a great guy.
GopherCon UK 2019: Advanced Testing Techniques, Alan Braithwaite, a short talk not so deep but covers lots of aspects.