Go Advanced Testing — Table-Driven Tests, Fuzzing, Test Coverage, and Benchmarks
In this tutorial, you will learn about Go Advanced Testing. We cover key concepts, practical examples, and best practices to help you master this topic.
Go advanced testing includes table-driven tests, fuzzing with testing.F, test coverage analysis, and subtest/subbenchmark organization.
What You'll Learn
- Table-driven test patterns
- Fuzz testing
- Test coverage and profiling
- Subtests and test helpers
Why It Matters
Advanced testing ensures robust software. Docker uses table-driven tests extensively. Kubernetes uses fuzzing for security. DodaZIP uses coverage analysis for quality gates.
Real-World Use
CI/CD quality gates, security vulnerability discovery, regression prevention, API Contract Testing.
flowchart LR
A["Advanced Testing"] --> B["Table-Driven"]
A --> C["Fuzzing"]
A --> D["Coverage"]
A --> E["Subtests"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Table-Driven Tests
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
err bool
}{
{name: "positive", a: 10, b: 2, want: 5},
{name: "negative", a: -6, b: 3, want: -2},
{name: "zero division", a: 5, b: 0, want: 0, err: true},
{name: "fraction", a: 3, b: 2, want: 1.5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := divide(tt.a, tt.b)
if tt.err && err == nil {
t.Error("expected error, got none")
}
if !tt.err && err != nil {
t.Errorf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("divide(%f, %f) = %f, want %f", tt.a, tt.b, got, tt.want)
}
})
}
}
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
Fuzz Testing
func FuzzHex(f *testing.F) {
seeds := []string{"hello", "world", "123", ""}
for _, s := range seeds {
f.Add(s)
}
f.Fuzz(func(t *testing.T, input string) {
encoded := hex.EncodeToString([]byte(input))
decoded, err := hex.DecodeString(encoded)
if err != nil {
t.Errorf("round trip failed: %v", err)
}
if string(decoded) != input {
t.Error("round trip mismatch")
}
})
}
Test Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out
// In CI
func TestMain(m *testing.M) {
c := m.Run()
// Fail if coverage below threshold
if c := testing.CoverMode(); c != "" {
// Parse coverage output
}
os.Exit(c)
}
Test Helper Functions
func TestUserValidation(t *testing.T) {
t.Helper()
assert := func(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
user := &User{Name: "Alice", Email: "alice@test.com"}
assert(t, user.Name, "Alice")
assert(t, user.Email, "alice@test.com")
}
Golden Files
func TestGoldenFile(t *testing.T) {
input := "hello"
got := process(input)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, []byte(got), 0644)
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Errorf("got %s, want %s", got, want)
}
}
Common Mistakes
1. Not Running Subtests in Parallel
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run subtests concurrently
// test body
})
}
2. Forgetting go vet
// Always run: go vet ./... before commits
3. Skipping Error Checks in Tests
Don't use _ for errors in tests. Always assert errors.
4. Flaky Tests
Avoid time.Sleep for synchronization. Use retry or deterministic timing.
5. Not Using -race
Always run go test -race ./... to detect data races.
Practice Questions
1. What is the benefit of table-driven tests? Clear test cases, easy to add new cases, reduces boilerplate, explicit naming.
2. How does fuzzing find bugs? Generates random inputs automatically. Tests edge cases developers might miss. Finds panics and logic errors.
3. What is test coverage? Percentage of code statements executed during tests. Use go test -cover to measure. 80%+ is a common target.
4. What are subtests? Nested tests with t.Run. Each subtest can run independently. Supports parallel execution and selective running.
Challenge: Write a table-driven test for a string reverse function.
Solution
func TestReverse(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "simple", input: "hello", want: "olleh"},
{name: "unicode", input: "你好", want: "好你"},
{name: "palindrome", input: "racecar", want: "racecar"},
{name: "empty", input: "", want: ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Reverse(tt.input); got != tt.want {
t.Errorf("Reverse() = %q, want %q", got, tt.want)
}
})
}
}
FAQ
{{< faq question="How do I run specific tests?" >}}
go test -run TestName/SubtestName runs matching tests. Use -v for verbose output. -count=1 disables Caching.
{{< /faq >}}
{{< faq question="What is the difference between Error and Fatal?" >}} t.Error marks test as failed but continues. t.Fatal marks as failed and stops the test immediately. {{< /faq >}}
{{< faq question="How do I test HTTP handlers?" >}} Use httptest.NewServer or httptest.NewRecorder. They provide in-memory HTTP test infrastructure. {{< /faq >}}
{{< faq question="What is go-cmp?" >}}
A package for comparing values in tests. Provides readable diffs: cmp.Diff(want, got). Better than reflect.DeepEqual.
{{< /faq >}}
{{< faq question="How do I set up test fixtures?" >}} Use testdata directory for files. Use t.TempDir() for temporary directories — automatically cleaned up. {{< /faq >}}
Try It Yourself
package main
import (
"testing"
)
func add(a, b int) int { return a + b }
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 with:
go test -v
Expected output — PASS with test result.
What's Next
Now that you understand advanced testing, explore benchmarking and profiling.
| Topic | Description | Link |
|---|---|---|
| Go Benchmarking | Performance Testing | {{< ref "35-benchmarking" >}} |
| Go Profiling | Performance profiling | {{< ref "36-profiling" >}} |
| Go Modules | Package management | {{< ref "37-modules" >}} |