Code of the Day
IntermediatePackages & testing

Testing basics

Write TestXxx functions with the testing package — t.Error, t.Fatal, t.Log, go test flags, and helper functions.

GoIntermediate11 min read
Recommended first
By the end of this lesson you will be able to:
  • Write a function that conforms to the TestXxx(t *testing.T) signature
  • Use t.Error, t.Errorf, t.Fatal, and t.Log appropriately
  • Run tests with go test and the -v flag
  • Write a test helper function that calls t.Helper()
  • Explain the difference between t.Error and t.Fatal

The Go standard library ships a testing framework. You don't need a third-party library to get started — just the testing package and go test. Good tests are one of the clearest signals of production-quality Go code, and the tooling is designed to make them easy to write.

The TestXxx signature

A test is any function in a _test.go file whose name starts with Test and takes a single *testing.T parameter:

// file: add_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    want := 5
    if got != want {
        t.Errorf("Add(2, 3) = %d; want %d", got, want)
    }
}

Run it:

go test ./...       # run all tests in the module
go test ./math/...  # run tests in the math package only
go test -v ./...    # verbose: print all test names and pass/fail

The -v flag is worth using locally — it shows each test name as it runs, so you can see exactly which test is slow or failing.

t.Error vs t.Fatal

Both mark the test as failed. The difference is whether the test continues:

FunctionEffect
t.Error(args...)Records failure, test keeps running
t.Errorf(format, args...)Like Error but formatted
t.Fatal(args...)Records failure, stops the test immediately
t.Fatalf(format, args...)Like Fatal but formatted

Use t.Fatal when a failure makes the rest of the test meaningless:

func TestOpenFile(t *testing.T) {
    f, err := os.Open("testdata/sample.txt")
    if err != nil {
        t.Fatalf("could not open test file: %v", err)
    }
    defer f.Close()
    // ... further assertions that require f to be valid
}

If Open fails, there is no point running assertions on ft.Fatal stops immediately rather than letting the test panic on a nil pointer.

t.Log

t.Log prints a message that appears only when the test fails (or when -v is set). Use it for diagnostic context rather than fmt.Println:

t.Logf("input: %v, got: %v", input, got)

This keeps test output clean on passing runs and provides detail when you need it.

Test helper functions

When multiple tests share setup or assertion logic, extract a helper:

func assertEqual(t *testing.T, got, want int) {
    t.Helper()   // <— marks this as a helper
    if got != want {
        t.Errorf("got %d; want %d", got, want)
    }
}

t.Helper() tells the testing framework that when this function reports a failure, the line number shown should be the caller's line, not the helper's. Without t.Helper(), all failures point to inside assertEqual rather than the line that called it.

Always call t.Helper() at the start of any test helper function. It is a one-liner that dramatically improves failure messages when tests break.

Test files and the _test.go convention

Go only compiles _test.go files when running go test. This means test code cannot interfere with your production binary. You can also use a _test package suffix for black-box testing:

// White-box: package math (can access unexported identifiers)
package math

// Black-box: tests the public API only
package math_test

Both approaches are valid. Black-box tests (package math_test) are stricter and give you better coverage of the exported API as callers experience it.

Useful go test flags

go test -run TestAdd         # run only tests matching the pattern
go test -count=1 ./...       # disable test result caching
go test -timeout 30s ./...   # fail if total test time exceeds 30 s
go test -cover ./...         # show statement coverage %
go test -coverprofile=c.out ./... && go tool cover -html=c.out

Go caches test results. If a test passes and nothing changes, go test returns the cached result without re-running. Use -count=1 to force a fresh run — useful in CI or when you suspect stale state.

Check your understanding

Knowledge check

  1. 1.
    A test opens a database connection and then runs five assertions. If the connection fails, which call is most appropriate?
  2. 2.
    Calling t.Helper() in a test helper function makes failures report the line in the caller rather than inside the helper.
  3. 3.
    You fix a bug and run go test ./... but it reports the old cached PASS result. What flag forces a fresh run?

Do it yourself

Write a Sum(nums []int) int function and a corresponding TestSum that checks at least three cases: empty slice, single element, and multiple elements.

go test -v ./...
go test -cover ./...

Where to go next

Now that you can write basic tests, the next lesson introduces — the idiomatic Go pattern for testing many inputs with minimal duplication — plus subtests and benchmarks.

Finished reading? Mark it complete to track your progress.

On this page