Skip to main content

Go Ep 14: Testing, Benchmarking & Fuzzing

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
go - This article is part of a series.
Part 14: This Article
Go includes a first-class test runner built into the go test CLI. You don’t need external test frameworks like Jest or JUnit to write unit tests, benchmarks, or fuzzers.

TL;DR (Quick Summary)
#

  • Test Files: Named *_test.go alongside the code file being tested.
  • Unit Tests (TestXxx(t *testing.T)): Standard unit tests. Failures called via t.Errorf or t.Fatalf.
  • Table-Driven Tests: Idiomatic Go testing pattern iterating over a slice of test case structs.
  • Benchmarks (BenchmarkXxx(b *testing.B)): Performance testing loop running b.N iterations.
  • Fuzzing (FuzzXxx(f *testing.F)): Automated random payload generation for security testing.

1. Table-Driven Unit Testing Pattern
#

Table-Driven Tests keep test cases clean, maintainable, and readable.

Create calculator_test.go:

package main

import "testing"

func Add(a, b int) int {
 return a + b
}

func TestAddTableDriven(t *testing.T) {
 // Define table of test cases
 tests := []struct {
  name     string
  a, b     int
  expected int
 }{
  {name: "Positive numbers", a: 2, b: 3, expected: 5},
  {name: "Negative numbers", a: -1, b: -4, expected: -5},
  {name: "Zero addition", a: 5, b: 0, expected: 5},
 }

 for _, tt := range tests {
  t.Run(tt.name, func(t *testing.T) {
   result := Add(tt.a, tt.b)
   if result != tt.expected {
    t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
   }
  })
 }
}

Run tests with verbose output and coverage:

go test -v -cover ./...

2. Benchmarking Performance (testing.B)
#

Benchmark functions measure code execution time and memory allocation per operation.

Add to calculator_test.go:

func BenchmarkAdd(b *testing.B) {
 // b.N is dynamically adjusted by the Go test runner until accurate metrics are achieved
 for i := 0; i < b.N; i++ {
  Add(100, 200)
 }
}

Run benchmarks with memory statistics:

go test -bench=. -benchmem

Expected Terminal Output:

goos: linux
goarch: amd64
pkg: github.com/username/go-masterclass
cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
BenchmarkAdd-8   1000000000          0.283 ns/op        0 B/op        0 allocs/op
PASS
ok      github.com/username/go-masterclass      0.345s

Notice: 0 B/op, 0 allocs/op proves Add allocates zero heap memory!


3. Automated Fuzz Testing (testing.F)
#

Fuzz testing feeds random binary and string data to your functions to find edge-case crashes.

func Reverse(s string) string {
 runes := []rune(s)
 for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
  runes[i], runes[j] = runes[j], runes[i]
 }
 return string(runes)
}

func FuzzReverse(f *testing.F) {
 testcases := []string{"Hello", "world", " ", "!123"}
 for _, tc := range testcases {
  f.Add(tc) // Seed corpus
 }

 f.Fuzz(func(t *testing.T, orig string) {
  rev := Reverse(orig)
  doubleRev := Reverse(rev)
  if orig != doubleRev {
   t.Errorf("Before: %q, after double reverse: %q", orig, doubleRev)
  }
 })
}

Run fuzzing for 10 seconds:

go test -fuzz=FuzzReverse -fuzztime=10s

4. Troubleshooting & Common Errors
#

Error 1: PASS Output Missing Failing Assertion
#

The Cause: Calling t.Log instead of t.Errorf or t.Fatalf. The Fix: Use t.Errorf to record failure without stopping other subtests, or t.Fatalf to stop execution immediately.


Summary & Next Steps
#

In this episode:

  • We wrote Table-Driven unit tests with testing.T.
  • We benchmarked performance and heap allocations with testing.B.
  • We discovered edge cases using automated fuzzing (testing.F).

In Episode 15: Building a Production REST API, we will assemble our final production web service!

go - This article is part of a series.
Part 14: This Article