Go Guide

Go, chapter by chapter · Lesson 10 of 62 · about 45 min

Testing

Write table-driven tests, measure speed with benchmarks, find edge cases with fuzzing, and make every DSA solution provably correct.

On this page · 10 sections
  1. 01The rules
  2. 02Table-driven tests
  3. 03Benchmarks
  4. 04Fuzzing
  5. 05Examples as documentation
  6. 06Useful flags
  7. 07A runnable demonstration
  8. 08Summary
  9. 09Check your understanding
  10. 10Practice in Go

The rules#

  • Tests live next to the code in files ending in _test.go.
  • A test is a function named TestXxx(t *testing.T).
  • t.Errorf(...) reports a failure and keeps going; t.Fatalf(...) reports and stops this test.
  • go test ./... runs all tests; go test -run TwoSum runs those whose name matches; -v shows each one.

Say this is your code:

Go
// twosum.go
package twosum
 
// TwoSum returns the indexes of the two numbers that add up to target, or nil.
func TwoSum(nums []int, target int) []int {
	seen := make(map[int]int, len(nums))
	for i, n := range nums {
		if j, ok := seen[target-n]; ok {
			return []int{j, i}
		}
		seen[n] = i
	}
	return nil
}

Table-driven tests#

Instead of one function per case, put the cases in a slice of structs and loop over them. This is the standard Go style:

Go
// twosum_test.go
package twosum
 
import (
	"slices"
	"testing"
)
 
func TestTwoSum(t *testing.T) {
	tests := []struct {
		name   string
		nums   []int
		target int
		want   []int
	}{
		{"basic", []int{2, 7, 11, 15}, 9, []int{0, 1}},
		{"answer at the end", []int{3, 2, 4}, 6, []int{1, 2}},
		{"same value twice", []int{3, 3}, 6, []int{0, 1}},
		{"negatives", []int{-3, 4, 3, 90}, 0, []int{0, 2}},
		{"no answer", []int{1, 2}, 7, nil},
		{"empty", nil, 1, nil},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := TwoSum(tc.nums, tc.target)
			if !slices.Equal(got, tc.want) {
				t.Errorf("TwoSum(%v, %d) = %v, want %v", tc.nums, tc.target, got, tc.want)
			}
		})
	}
}

Each case runs as a named subtest, so a failure tells you exactly which case broke, and go test -run 'TwoSum/negatives' reruns just that one.

$ go test -v
=== RUN   TestTwoSum
=== RUN   TestTwoSum/basic
...
--- PASS: TestTwoSum (0.00s)
PASS

Which cases to write

Before coding any algorithm, list these:

CaseWhy
empty inputloops that never run, nums[0] panics
one elementoff-by-one errors
duplicatesmaps that overwrite, pointers that skip
negatives and zerosign assumptions
already sorted / reverse sortedworst cases for some algorithms
the largest allowed inputtimeouts and overflow
no valid answerwhat you return when nothing matches

Writing the table first forces you to decide these before you write the solution. That habit alone fixes most wrong answers on LeetCode.

Benchmarks#

A benchmark measures speed. Go runs it enough times to get a stable number:

Go
func BenchmarkTwoSum(b *testing.B) {
	nums := rand.Perm(10_000) // setup is not timed
	for b.Loop() {           // Go 1.24+; older code uses: for i := 0; i < b.N; i++
		TwoSum(nums, 19_997)
	}
}
$ go test -bench=TwoSum -benchmem
BenchmarkTwoSum-4   5740   190347 ns/op   295569 B/op   34 allocs/op

Read it as: each call took about 190 microseconds and allocated about 296 KB in 34 allocations. Without the size hint in make(map[int]int, len(nums)), the same benchmark took about 413 microseconds: the map had to grow and rehash repeatedly. Numbers vary by machine, but the comparison holds. Benchmarks are how you prove Big-O claims in Module 2.

Fuzzing#

A fuzz test generates random inputs and checks a property that must always hold:

Go
func FuzzReverse(f *testing.F) {
	f.Add("hello") // seed input
	f.Fuzz(func(t *testing.T, s string) {
		if Reverse(Reverse(s)) != s {
			t.Errorf("double reverse changed %q", s)
		}
	})
}

Run go test -fuzz=FuzzReverse and Go mutates inputs until something fails, then saves the failing input so it becomes a permanent regression test.

Examples as documentation#

A function named ExampleXxx with an // Output: comment is both documentation and a test:

Go
func ExampleTwoSum() {
	fmt.Println(TwoSum([]int{2, 7, 11, 15}, 9))
	// Output: [0 1]
}

go test checks the output, and pkg.go.dev shows it in your package docs.

Useful flags#

CommandPurpose
go test ./...everything
go test -run Name -vone test, verbose
go test -coverpercent of lines executed
go test -racedetect data races in concurrent code
go test -bench . -benchmemall benchmarks with memory stats
go test -count=1ignore cached results

A runnable demonstration#

Test files run only under go test, but you can see the same table-driven idea in a plain program. Paste this into the Playground:

table.go
Go
package main
 
import (
	"fmt"
	"slices"
)
 
func TwoSum(nums []int, target int) []int {
	seen := make(map[int]int, len(nums))
	for i, n := range nums {
		if j, ok := seen[target-n]; ok {
			return []int{j, i}
		}
		seen[n] = i
	}
	return nil
}
 
func main() {
	tests := []struct {
		name   string
		nums   []int
		target int
		want   []int
	}{
		{"basic", []int{2, 7, 11, 15}, 9, []int{0, 1}},
		{"same value twice", []int{3, 3}, 6, []int{0, 1}},
		{"negatives", []int{-3, 4, 3, 90}, 0, []int{0, 2}},
		{"no answer", []int{1, 2}, 7, nil},
		{"empty", nil, 1, nil},
	}
	failed := 0
	for _, tc := range tests {
		got := TwoSum(tc.nums, tc.target)
		status := "PASS"
		if !slices.Equal(got, tc.want) {
			status = "FAIL"
			failed++
		}
		fmt.Printf("%-4s %-16s got %v\n", status, tc.name, got)
	}
	fmt.Println("failed:", failed)
}
Output
PASS basic            got [0 1]
PASS same value twice got [0 1]
PASS negatives        got [0 2]
PASS no answer        got []
PASS empty            got []
failed: 0

Summary#

  • Tests are TestXxx(t *testing.T) functions in _test.go files, run with go test.
  • Table-driven tests with t.Run subtests are the Go standard.
  • List edge cases first: empty, one element, duplicates, negatives, extremes, no answer.
  • Benchmarks (BenchmarkXxx, b.Loop, -benchmem) prove performance claims.
  • Fuzz tests and Example functions catch surprises and document behavior.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.Which file name will go test pick up?

  2. Q2.What is the difference between t.Errorf and t.Fatalf?

  3. Q3.What does -benchmem add to benchmark output?

  4. Q4.Why is a table-driven test better than several copy-pasted test functions?

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

  • Test everything so farbuild

    Add table tests to every function in your godsa repo. Reach 90% coverage with go test -cover ./....

    Show hint

    go test -coverprofile=c.out ./... && go tool cover -html=c.out shows uncovered lines in the browser.

  • Benchmark three duplicates checksbuild

    Benchmark an O(n²) loop, a sort-based and a map-based duplicates check at n = 100, 10,000 and 1,000,000.

    Show hint

    Use b.Run(fmt.Sprint(n), ...) sub-benchmarks so one command compares all sizes.

  • Fuzz a reverse functionbuild

    Write Reverse(s string) string, then a fuzz test checking that reversing twice returns the original. Fix what the fuzzer finds.

    Show hint

    The fuzzer will find invalid UTF-8. Decide whether to reverse bytes or runes, and check utf8.ValidString.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.