I wish I could sleep like this, maybe even hibernate. But that’s too Java. Photo by Daniele Levis Pelusi on Unsplash

Motivation

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.

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:

Test-driven learning (LGTM means looks good to me)

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.

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.

Go FAQ: Where is my favorite helper function for testing?

Let’s see this via an example:

write test just like non-test code

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.

Package testing: Overview

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.

Go FAQ: Why does Go not have assertions?

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

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 (…)

Package testing: Subtests and Sub-benchmarks

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

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.

README.md from go-cmp

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:

test coverage result in web browser

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.

Senior Software Engineer @ Instana, Programming / Human Language Enthusiast. Located in Munich, Germany

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store