In this post I’m just going to maintain a list of notes for Go testing that I seem to commonly need to reference. It will also serve as an index for the posts related to testing that I have to commonly look up as well. Here is a quick listing of the table of contents:
Basics
Just a quick reminder of how to write tests, benchmarks, and examples. A test is written as follows:
func TestThing(t *testing.T) {}
Benchmarks are written as follows:
func BenchmarkThing(b *testing.B) {
for i := 0; i < b.N; i++ {
// Do Thing
}
}
To run benchmarks, ensure you use the bench flag: go test -bench=.
with the directory that contains the benchmarks specified. Examples are written as follows:
func Examplething() {
Thing()
// Output:
// thing happened
}
Test assertions and descriptions are as follows (most commonly used):
t.Error
/t.Errorf
: equivalent tot.Log
followed byt.Fail
, which means that the failure message is printed out, but the test continues running.t.Fatal
/t.Fatalf
: equivalent tot.Log
followed byt.FailNow
, which means the failure message is printed but the test is canceled at that point (all deferred calls will be executed after this step).t.Helper
: marks the test is a helper, when printing file and line info, the function will be skipped. Usually used to make common assertions or perform setup or tear down.t.Skip
/t.Skipif
: marks the test as skipped, though the test will still fail if anyError
was called before the skip.
Ref: Package testing
Table Driven Tests
Table driven tests use several language features including composite literals and anonymous structs to write related tests in a compact form. The most compact form of the tests looks like this:
var fibTests = []struct{
n int //input
expected int // expected result
}{
{1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}, {7, 13},
}
Of course it is also possible to define an internal struct in the test package (if using pkg_test
) for reusable test construction. Hooking it up is as simple as:
func TestFig(t *testing.T) {
for _, tt := range fibTests {
actual := Fib(tt.n)
if actual != expected {
t.Errorf("Fig(%d): expected %d, actual %d", tt.n, tt.expected, actual)
}
}
}
Ref: Dave Cheney — Writing table driven tests in Go
Fixtures
When using the Go testing package, the test binary will be executed with its working directory set to the source directory of the package being tested. Additionally, the Go tool will ignore directories that start with a period, an underscore, or matches the word testdata
. This means that you can create a directory called testdata
and store fixtures there. You can then load data as follows:
func loadFixture(t *testing.T, name string) []byte {
path := filepath.Join("testdata", name)
bytes, err := ioutil.Readfile(path)
if err != nil {
t.Fatalf("could not open test fixture %s: %s", name, err)
}
return bytes
}
Ref: Dave Cheney — Test fixtures in Go
Golden Files
When testing complicated or large output, you can save the data as an output file named .golden
and provide a flag for updating it:
var update = flag.Bool("update", false, "update .golden files")
func TestSomething(t *testing.T) {
actual := doSomething()
golden := filepath.Join("testdata", tc.Name+".golden")
if *update {
ioutil.WriteFile(golden, actual, 0644)
}
expected, _ := ioutil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
// Fail!
}
}
Ref: Povilas Versockas — Go advanced testing tips & tricks
Frameworks
No Framework
Ben Johnson makes a good argument for not using a testing framework. Go has a simple yet powerful testing framework. Frameworks are a barrier to entry for contributors to code. Frameworks require more dependencies to be fetched and managed. To reduce the verbosity, you can include simple test assertions as follows:
func assert(tb testing.TB, condition bool, msg string)
func ok(tb testing.TB, err error)
func equals(tb testing.TB, exp, act interface{})
This way you can write tests as:
func TestSomething(t *testing.T) {
value, err := DoSomething()
ok(t, err)
equals(t, 100, value)
}
I certainly like the simplicity of this idea and on many of my small packages I simply write tests like this. However, in larger projects, it feels like test organization can quickly get out of control and I don’t know what I’ve tested and where.
Ref: Ben Johnson — Structuring Tests in Go Ref: Testing Functions for Go
Ginkgo & Gomega
Many of my projects have started off using Ginkgo and Gomega for testing. Ginkgo provides BDD style testing to write expressive and well organized tests. Gomega provides a matching library for performing test-related assertions.
To bootstrap a test suite (after installing the libraries with go get
) you would run ginkgo bootstrap
. This creates the test suite which runs the tests. You can then generate tests by running ginkgo generate thing
to create thing_test.go
with the test stub already inside it:
package thing_test
import (
. "/path/to/thing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Thing", func() {
var thing Thing
BeforeEach(func() {
thing = new(Thing)
})
It("should do something", func() {
Ω(thing.Something()).Should(Succeed())
})
})
While I do like the use of this test framework, it’s primarily for the organization of the tests and the runner.
Helpers
Helper functions can be marked with t.Helper()
, which excludes their line and signature information from the test error traceback. They can be used to do setup for the test case, unrelated error checks, and can even clean up after themselves!
Temporary Directories
Often, I need a temporary directory to store a database in or write files to. I can create the temporary directory with this helper function, which also returns a function to cleanup the temporary directories.
const tmpDirPrefix = "mytests"
func tempDir(t *testing.T, name string) (path string, cleanup func()) {
t.Helper()
tmpDir, err = ioutil.TempDir("", tmpDirPrefix)
if err != nil {
t.Fatalf("could not create temporary directory: %s", err)
}
return filepath.Join(tmpDir, name), func() {
err = os.RemoveAll(tmpDir)
if err != nil {
t.Errorf("could not remove temporary directory: %s", err)
}
}
}
This can be used with the cleanup function pretty simply:
func TestThing(t *testing.T) {
dir, cleanup := tempDir(t, "db")
defer cleanup()
...
}
Another version that I have in some of my tests creates a temporary directory for all of the tests, stored in a variable at the top level, any caller asking for a directory can create it, but it won’t be overridden if if already exists; then any test that cleans up will clean up that directory.
Sources and References
- Package testing
- Dave Cheney — Writing table driven tests in Go
- Dave Cheney — Test fixtures in Go
- Ben Johnson — Structuring Tests in Go
- Povilas Versockas — Go advanced testing tips & tricks
Not related to testing, but saved for reference for a later godoc notes post: