Testing#
Go ships its own test runner (go test) and assertion-free
testing package (testing). The convention is table-driven
tests, one function per behaviour, sub-tests via t.Run.
The race detector (go test -race) and the fuzzer (go test
-fuzz) come with the toolchain.
testify is the most common third-party addition (rich
assertions, mocks); for greenfield code the operator often
stays on stdlib plus cmp.Diff from github.com/google/go-cmp.
For
errors.Is / errors.As in test assertions, see
Errors.
Layout#
Tests live alongside source in *_test.go files. Test
functions take *testing.T and start with Test.
// string_helpers.go
package strutil
func Trim(s string) string { return strings.TrimSpace(s) }
// string_helpers_test.go
package strutil
import "testing"
func TestTrim(t *testing.T) {
got := Trim(" hi ")
if got != "hi" {
t.Errorf("Trim: got %q, want %q", got, "hi")
}
}
$ go test ./...
ok github.com/op/scan/strutil 0.003s
Table-driven#
The default structure for any function with a small input space.
func TestTrim(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"clean", "hi", "hi"},
{"trailing", "hi ", "hi"},
{"leading", " hi", "hi"},
{"both", " hi ", "hi"},
{"empty", "", ""},
{"whitespace", " ", ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := Trim(tc.in)
if got != tc.want {
t.Errorf("Trim(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
t.Run produces a sub-test you can filter on with
go test -run TestTrim/empty.
Setup and teardown#
Avoid global setup. t.Cleanup registers a
function to run when the test (or sub-test) finishes.
func TestServer(t *testing.T) {
srv := startServer(t)
t.Cleanup(func() { srv.Close() })
// …
}
func startServer(t *testing.T) *Server {
t.Helper() // report errors from the caller
s, err := NewServer(":0")
if err != nil { t.Fatal(err) }
return s
}
t.Helper tells the test runner this function is plumbing;
failures point at the caller, not the helper line.
Fixtures#
For per-test temporary directories, t.TempDir returns a
fresh path and cleans up automatically.
func TestWriteAtomic(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "out.txt")
if err := WriteAtomic(path, []byte("hi")); err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(path)
if string(got) != "hi" { t.Errorf("got %q", got) }
}
Comparing complex values#
For deep-equality on structs and maps, cmp.Diff from
github.com/google/go-cmp produces readable diffs.
import "github.com/google/go-cmp/cmp"
func TestParse(t *testing.T) {
got, _ := Parse(input)
want := &Config{Host: "0.0.0.0", Port: 80}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("Parse mismatch (-want +got):\n%s", diff)
}
}
For floating point, cmpopts.EquateApprox.
Assertion libraries#
testify is the most common; pick the
require package to halt on first failure or assert to
collect them.
$ go get github.com/stretchr/testify
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParse(t *testing.T) {
c, err := Parse(input)
require.NoError(t, err)
assert.Equal(t, "0.0.0.0", c.Host)
assert.Equal(t, 80, c.Port)
}
Race detector#
-race instruments the binary to catch concurrent reads /
writes on the same location. Mandatory in CI for any code
touching goroutines.
$ go test -race ./...
Fuzz tests#
Go 1.18+ ships a fuzzer. The operator declares seed inputs and the fuzz function generates more.
func FuzzParse(f *testing.F) {
f.Add("host=0.0.0.0\nport=80")
f.Add("")
f.Add("\x00\x01")
f.Fuzz(func(t *testing.T, s string) {
_, err := Parse(s) // must not panic
_ = err // any error is fine
})
}
$ go test -fuzz=FuzzParse -fuzztime=30s
A crashing input gets saved under testdata/fuzz/FuzzParse/;
running go test after replays them as regression cases.
Benchmarks#
Benchmarks are functions starting with Benchmark; the
runner picks b.N to stabilise.
func BenchmarkParse(b *testing.B) {
data := []byte(loadFixture())
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = Parse(data)
}
}
$ go test -bench=. -benchmem ./...
Coverage#
$ go test -cover ./...
$ go test -coverprofile=cover.out ./...
$ go tool cover -html=cover.out
HTTP tests#
net/http/httptest lets the operator stand up a real HTTP
server backed by a test http.Handler.
import "net/http/httptest"
func TestHandler(t *testing.T) {
srv := httptest.NewServer(myHandler)
defer srv.Close()
resp, err := http.Get(srv.URL + "/ping")
if err != nil { t.Fatal(err) }
if resp.StatusCode != 200 { t.Errorf("status %d", resp.StatusCode) }
}
For end-to-end tests of HTTP clients (not servers),
httptest.NewServer plays the role of the upstream API.