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
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 TwoSumruns those whose name matches;-vshows each one.
Say this is your code:
// 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:
// 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)
ifslices.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)
PASSWhich cases to write
Before coding any algorithm, list these:
| Case | Why |
|---|---|
| empty input | loops that never run, nums[0] panics |
| one element | off-by-one errors |
| duplicates | maps that overwrite, pointers that skip |
| negatives and zero | sign assumptions |
| already sorted / reverse sorted | worst cases for some algorithms |
| the largest allowed input | timeouts and overflow |
| no valid answer | what 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:
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/opRead 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:
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:
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#
| Command | Purpose |
|---|---|
go test ./... | everything |
go test -run Name -v | one test, verbose |
go test -cover | percent of lines executed |
go test -race | detect data races in concurrent code |
go test -bench . -benchmem | all benchmarks with memory stats |
go test -count=1 | ignore 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:
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"
ifslices.Equal(got, tc.want) {
status = "FAIL"
failed++
}
fmt.Printf("%-4s %-16s got %v\n", status, tc.name, got)
}
fmt.Println("failed:", failed)
}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: 0Summary#
- Tests are
TestXxx(t *testing.T)functions in_test.gofiles, run withgo test. - Table-driven tests with
t.Runsubtests 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
Examplefunctions catch surprises and document behavior.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Which file name will
go testpick up?Q2.What is the difference between
t.Errorfandt.Fatalf?Q3.What does
-benchmemadd to benchmark output?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.outshows 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.