13 — Testing

Go's testing support is built into the toolchain, not a library you pick. This page shows the functions under test (runnable) and explains the harness features around them.

1. The functions under test

Testing in Go lives in the go command: a file ending _test.go with func TestXxx(t *testing.T) functions, run with go test. Before the test machinery, here are the small functions a test suite would exercise — this part is runnable:

package main

import (
    "fmt"
    "strings"
)

func Classify(n int) string {
    switch {
    case n < 0:
        return "negative"
    case n == 0:
        return "zero"
    default:
        return "positive"
    }
}

func Normalize(s string) string {
    return strings.ToLower(strings.TrimSpace(s))
}

func IsPalindrome(s string) bool {
    rs := []rune(Normalize(s))      // fold case/space, compare by rune
    for i, j := 0, len(rs)-1; i < j; i, j = i+1, j-1 {
        if rs[i] != rs[j] {
            return false
        }
    }
    return true
}

func main() {
    fmt.Println(Classify(-3), Classify(0), Classify(7))
    fmt.Printf("%q\n", Normalize("  Hello  "))
    fmt.Println(IsPalindrome("  RaceCar  "), IsPalindrome("hello"))
}

Output:

negative zero positive
"hello"
true false

2. The test harness (reference)

The code below runs under go test, not here — a browser sandbox executes a main, not a test binary. These are the patterns worth knowing:

// Table-driven subtests: a slice of cases, each run with t.Run so it's
// named and individually selectable (go test -run TestClassify/zero).
func TestClassify(t *testing.T) {
    cases := []struct {
        name string
        in   int
        want string
    }{
        {"negative", -3, "negative"},
        {"zero", 0, "zero"},
        {"positive", 7, "positive"},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            if got := Classify(c.in); got != c.want {
                t.Errorf("Classify(%d) = %q, want %q", c.in, got, c.want)
            }
        })
    }
}

// t.Helper() makes failures point at the CALLER, not inside the helper.
func assertEqual(t *testing.T, got, want string) {
    t.Helper()
    if got != want {
        t.Errorf("got %q, want %q", got, want)
    }
}

// Benchmark: go test -bench=.
func BenchmarkIsPalindrome(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = IsPalindrome("A man a plan a canal Panama")
    }
}

// Fuzz: go test -fuzz=Fuzz. Seeds a few inputs; the fuzzer invents more.
func FuzzNormalize(f *testing.F) {
    f.Add("Hello")
    f.Fuzz(func(t *testing.T, s string) {
        if Normalize(Normalize(s)) != Normalize(s) {
            t.Error("Normalize is not idempotent")
        }
    })
}

Recap

ConceptIn Go
Test file / function*_test.go; func TestXxx(t *testing.T)
Rungo test, -v, ./...
Fail / fatalt.Errorf (continue) / t.Fatalf (stop)
Table-drivenCase slice + t.Run(name, fn)
Helper line numberst.Helper()
Benchmark / fuzzBenchmark* / Fuzz* — built in

Next: 14 — Modules & Tooling.