Writing Tests

Go has testing built in: put TestXxx functions in a _test.go file and run go test ./... to check every package.

Syntaxfunc TestXxx(t *testing.T) — in a file named *_test.go; run with: go test ./...

A test is code that checks other code. You write it once, and the toolchain runs it for you as often as you like. When you change something later, the tests tell you what you broke.

Go has testing built in. There is nothing to install and no configuration file. A test lives in a file whose name ends in _test.go, in the same directory as the code it tests. That file belongs to the package while testing, and is left out of the program you build.

Writing one

A test is a function whose name begins with Test and that takes one argument, t *testing.T. The name after Test must start with an upper-case letter. t is how you report trouble. t.Errorf records a failure and lets the test carry on. t.Fatalf records a failure and stops that test there.

A test that reports nothing has passed. There is no assert function in the standard library. You compare values with if and write the message yourself, by convention in the form got X, want Y. The short form if got := Sum(nil); got != 0 { declares and tests in one line; the longhand is got := Sum(nil) on its own line followed by if got != 0 {. They are the same thing, except that the short form's got only exists inside the if.

Running them

go test ./... runs the tests in this directory and every directory below it. -v prints each test name. -run takes a pattern and runs only the tests whose names match it.

Example

Example · go
// File: sum_test.go — run it with: go test -v ./...
package sum

import "testing"

// Sum adds every number in the list and returns the total.
func Sum(numbers []int) int {
	total := 0
	for _, n := range numbers {
		total += n
	}
	return total
}

func TestSum(t *testing.T) {
	got := Sum([]int{1, 2, 3})
	want := 6
	if got != want {
		// Errorf records the failure, then the test keeps running.
		t.Errorf("Sum([1 2 3]) = %d, want %d", got, want)
	}
}

func TestSumEmpty(t *testing.T) {
	// Same check written short: declare and compare in one line.
	if got := Sum(nil); got != 0 {
		t.Errorf("Sum(nil) = %d, want 0", got)
	}
}

// Output:
// === RUN   TestSum
// --- PASS: TestSum (0.00s)
// === RUN   TestSumEmpty
// --- PASS: TestSumEmpty (0.00s)
// PASS
// ok  	sum	0.002s

When to use it

  • Pin down a bug: write a failing test that reproduces it, then fix the code until it passes.
  • Check a parsing or formatting function against the handful of inputs you care about before wiring it into anything.
  • Run go test ./... in continuous integration so a broken package cannot be merged.

More examples

Checking that a function returns the right error

Half of testing is checking the unhappy path. Compare errors with errors.Is rather than ==, so a wrapped error still matches. Use t.Fatalf when carrying on would be meaningless: if Divide returned an error here, the value next to it is not worth checking.

Example · go
package calc

import (
	"errors"
	"testing"
)

var ErrDivideByZero = errors.New("divide by zero")

func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, ErrDivideByZero
	}
	return a / b, nil
}

func TestDivideByZero(t *testing.T) {
	_, err := Divide(1, 0)
	if !errors.Is(err, ErrDivideByZero) {
		t.Fatalf("Divide(1, 0) error = %v, want %v", err, ErrDivideByZero)
	}
}

func TestDivideOK(t *testing.T) {
	got, err := Divide(9, 3)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != 3 {
		t.Errorf("Divide(9, 3) = %d, want 3", got)
	}
}

The commands you actually type

These five flags cover almost every day. -run takes a pattern, not an exact name, so -run TestSum also matches TestSumEmpty. Reach for -count=1 whenever a test result looks impossibly fast: it is the cache, not your code.

Example · bash
go test ./...                    # every package in this directory and below
go test -v ./internal/sum        # one package, printing each test name
go test -run TestSumEmpty ./...  # only tests whose name matches the pattern
go test -count=1 ./...           # ignore the cache and really run them
go test -race ./...              # same tests, with the race detector on
go test -cover ./...             # report what fraction of lines ran

Testing a package from the outside

A test file in the same directory may declare the package name with a _test suffix. It then sees only the exported names, exactly as another package would, which is a good way to check that the public surface is usable. The import path must be the module path from go.mod plus the directory.

Example · go
// File: sum/sum_blackbox_test.go
package sum_test

import (
	"testing"

	"example.com/app/sum"
)

func TestSumFromOutside(t *testing.T) {
	if got := sum.Sum([]int{2, 2}); got != 4 {
		t.Errorf("sum.Sum([2 2]) = %d, want 4", got)
	}
}

Discussion

  • Be the first to comment on this lesson.