Testing with go test
Prerequisites: Building an HTTP server
Go has testing built in — no framework to install.
- Tests live in files ending in
_test.go, next to the code they test, usually in the same package. - A test is a function named
TestXxx(t *testing.T). - Report failures with
t.Errorf(records the failure and keeps going) ort.Fatalf(records it and stops this test). - Table-driven tests put the cases in a slice of structs and loop over
them.
t.Run(name, ...)turns each case into a named subtest. go test ./...runs every test in the module;-vshows each test,-runfilters by name,-coverreports coverage,-raceenables the race detector.
Syntax
func TestSomething(t *testing.T) {
got := Something()
if got != want {
t.Errorf("Something() = %v, want %v", got, want)
}
}
Example
calc/calc.go:
package calc
import "errors"
var ErrDivideByZero = errors.New("divide by zero")
// Divide returns a / b, truncated toward zero.
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
calc/calc_test.go:
package calc
import (
"errors"
"testing"
)
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
want int
wantErr error
}{
{name: "even division", a: 10, b: 2, want: 5},
{name: "truncates", a: 7, b: 2, want: 3},
{name: "negative", a: -9, b: 3, want: -3},
{name: "by zero", a: 1, b: 0, wantErr: ErrDivideByZero},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("Divide(%d, %d) error = %v, want %v", tt.a, tt.b, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("Divide(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
Expected output
$ go test -v ./calc
=== RUN TestDivide
=== RUN TestDivide/even_division
=== RUN TestDivide/truncates
=== RUN TestDivide/negative
=== RUN TestDivide/by_zero
--- PASS: TestDivide (0.00s)
--- PASS: TestDivide/even_division (0.00s)
--- PASS: TestDivide/truncates (0.00s)
--- PASS: TestDivide/negative (0.00s)
--- PASS: TestDivide/by_zero (0.00s)
PASS
ok example.com/hello/calc 0.002s
$ go test -cover ./calc
ok example.com/hello/calc 0.002s coverage: 100.0% of statements
The module path and timings will differ on your machine. Spaces in subtest
names become underscores, so you can run one case with
go test -run 'TestDivide/by_zero' ./calc.
If a case fails, the output names the exact subtest and your message:
--- FAIL: TestDivide (0.00s)
--- FAIL: TestDivide/truncates (0.00s)
calc_test.go:28: Divide(7, 2) = 4, want 3
FAIL
Common mistakes
- Naming the file
calc_tests.goor the functiontestDivide— the file must end in_test.goand the function must start withTestfollowed by an uppercase letter (or nothing), orgo testignores it. - Vague failure messages such as
t.Error("wrong"). Include the input, what you got and what you wanted. - Using
t.Fatalfwheret.Errorfwould do, which hides later failures in the same test. - Tests that depend on each other or on execution order. Each test should set up what it needs.
Practice
Write a function Reverse(s string) string that reverses a string by runes
(so "héllo" becomes "olléh"), and a table-driven test covering an empty
string, "go", and "héllo".
Show solution
reverse.go:
package reverse
// Reverse returns s with its runes in reverse order.
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
reverse_test.go:
package reverse
import "testing"
func TestReverse(t *testing.T) {
tests := []struct {
name, in, want string
}{
{name: "empty", in: "", want: ""},
{name: "ascii", in: "go", want: "og"},
{name: "multi-byte", in: "héllo", want: "olléh"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Reverse(tt.in); got != tt.want {
t.Errorf("Reverse(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}