Skip to content
AgileLogics
7 min read

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) or t.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; -v shows each test, -run filters by name, -cover reports coverage, -race enables 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.go or the function testDivide — the file must end in _test.go and the function must start with Test followed by an uppercase letter (or nothing), or go test ignores it.
  • Vague failure messages such as t.Error("wrong"). Include the input, what you got and what you wanted.
  • Using t.Fatalf where t.Errorf would 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)
            }
        })
    }
}