8. Testing

Table-driven tests, race detector, subtests, coverage, benchmarks, and fuzzing basics.

Question: How do you detect data races in a Go application?

Answer: Go has a built-in race detector. You can enable it by adding the -race flag when running your code, most commonly with go test -race.

Explanation: The race detector instruments the code to monitor memory access. If it detects that two goroutines are accessing the same memory location concurrently without synchronization, and at least one of the accesses is a write, it will report a data race with a stack trace. It is a powerful tool that should be used regularly to build robust concurrent applications.

Question: What is a "table-driven test" in Go and what are its benefits?

Answer: A table-driven test is a common pattern where you define a slice of test cases (the "table"). Each element is a struct containing the inputs and the expected output for a single test. The test logic then iterates over this slice and executes the same assertions for each case.

Explanation: This pattern makes it easy to add, remove, or modify test cases with minimal code changes. It also makes the tests highly readable, as all the inputs and expected outcomes are declared declaratively in one place.

func TestAdd(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"positive", 1, 2, 3},
        {"zero", 0, 0, 0},
        {"negative", -1, 1, 0},
    }
    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            got := Add(tc.a, tc.b)
            if got != tc.want {
                t.Errorf("got %d, want %d", got, tc.want)
            }
        })
    }
}

Question: How do you measure coverage and write benchmarks?

Answer: Use go test -cover for coverage, and define func BenchmarkX(b *testing.B) for benchmarks.

Explanation: Benchmarks run the body b.N times. Combine with the race detector where relevant using -race.

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = Add(1, 2)
    }
}

Question: How do you mark helper functions and clean up in tests?

Answer: Use t.Helper() inside helpers so failures report the caller line. Use t.Cleanup(func(){ ... }) to register cleanup.

Question: How do you run subtests in parallel safely?

Answer: Call t.Parallel() at the start of each subtest and capture loop variables.

for _, tc := range cases {
    tc := tc
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        // test body
    })
}

Question: What is fuzzing in Go and how to write a fuzz test?

Answer: Fuzzing (Go 1.18+) generates random inputs to find crashes/invariants violations.

func FuzzParseIP(f *testing.F) {
    f.Add("127.0.0.1")
    f.Fuzz(func(t *testing.T, s string) {
        _ = net.ParseIP(s)
    })
}

Question: How do you create temporary directories/files safely in tests?

Answer: Use t.TempDir() for a per-test temporary directory and os.CreateTemp for files.

Explanation: TempDir is auto-cleaned; avoid hard-coded paths.

Question: How do you run setup/teardown around the whole package test suite?

Answer: Implement func TestMain(m *testing.M) to run code before/after tests.

func TestMain(m *testing.M) {
    code := m.Run()
    os.Exit(code)
}