Testing & Tooling
Go’s built-in testing story
Section titled “Go’s built-in testing story”Most languages require a third-party testing framework. Go ships everything you need inside the standard library. The testing package provides *testing.T for unit tests, *testing.B for benchmarks, and *testing.F for fuzzing — all driven by a single command: go test.
The go toolchain that ships with every Go installation also includes:
| Tool | Purpose |
|---|---|
go test | Run tests and benchmarks |
go build | Compile packages and binaries |
go run | Compile and run in one step |
go fmt | Format source code (wraps gofmt) |
go vet | Static analysis for common mistakes |
go mod | Module and dependency management |
go doc | Browse package documentation |
What this module covers
Section titled “What this module covers”- go-test — Writing
_test.gofiles,func TestXxx(t *testing.T), table-driven tests witht.Run,t.Parallel, fuzzing with*testing.F, and everything else*testing.Tgives you. - benchmarks — Measuring performance with
func BenchmarkXxx(b *testing.B), reading-benchmemoutput, and comparing runs withbenchstat. - modules — Managing dependencies with
go mod init,go.mod,go.sum,go get, and how version resolution actually works. - go-tooling — The everyday commands:
go run,go build,go fmt,go vet’s bug-catching analyzers,go doc, and the wider linting ecosystem.
The testing package family
Section titled “The testing package family”flowchart TD T["testing package"] --> A["*testing.T — unit tests, table-driven, t.Parallel"] T --> B["*testing.B — benchmarks, b.Loop, benchstat"] T --> F["*testing.F — fuzzing, mutates inputs to find edge cases"] A --> G["go test ./..."] B --> G F --> G
A function worth testing
Section titled “A function worth testing”The best way to appreciate Go’s testing tools is to start with a pure function — one with no side effects, no I/O, just input and output. The playground below runs Fibonacci from main. In the next lesson you will write a proper _test.go file for exactly this kind of function — including a fuzz test, since Fibonacci and IsPrime both take a single int and are exactly the shape *testing.F is good at hammering with edge-case inputs.
package main
import "fmt"
// Fibonacci returns the nth Fibonacci number (0-indexed).// fib(0)=0, fib(1)=1, fib(2)=1, fib(3)=2, ...func Fibonacci(n int) int { if n <= 1 { return n } return Fibonacci(n-1) + Fibonacci(n-2)}
// IsPrime reports whether n is a prime number.func IsPrime(n int) bool { if n < 2 { return false } for i := 2; i*i <= n; i++ { if n%i == 0 { return false } } return true}
func main() { // Print the first 10 Fibonacci numbers fmt.Println("Fibonacci sequence:") for i := 0; i < 10; i++ { fmt.Printf(" fib(%d) = %d\n", i, Fibonacci(i)) }
// Find primes up to 30 fmt.Println("Primes up to 30:") for n := 2; n <= 30; n++ { if IsPrime(n) { fmt.Printf(" %d\n", n) } }} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Built-in testing package | No install step, one standard API across every Go project | No assertion library — you write if got != want yourself |
| Table-driven tests | Test many cases in one function, easy to add a case | A very long table gets hard to read |
go test ./... | Runs every package’s tests in the module in one command | Parallel tests can race on shared state if you are not careful |
Race detector (-race) | Catches concurrency bugs automatically | 2-10x slower — run it in CI, not on every local run |
Common Misconceptions
Section titled “Common Misconceptions”- You need a testing framework like Python or Node’s — the Go standard library already has everything you need; there is no need to install testify or gomega.
- 100% coverage means the code is correct — coverage only measures whether a line ran, not whether the behavior is correct — a test with no assertions can still reach 100% coverage.
go testis slow —go testcaches results; if nothing changed, it returns the cached result instantly.
Reading AI-generated code
Section titled “Reading AI-generated code”Before this module goes into depth, start with the single most common generated-test failure mode: a test that runs, prints PASS, and checks nothing.
- Does every
Test*function actually callt.Error/t.Errorf/t.Fatal/t.Fatalfsomewhere, or does it just call the function under test and let anilerror silently mean success? - If the function returns a value, is that value ever compared against an expected result — or only checked for “did not panic”?
- Does a benchmark or fuzz test’s assertion logic actually depend on the input, or would it pass identically no matter what the function under test returned?
🐛 Spot the bug
func TestParseConfig(t *testing.T) { cfg := ParseConfig("timeout=30") fmt.Println(cfg) // just prints — never compares to anything}Show the answer
This test calls ParseConfig, prints the result, and ends — there is no t.Error, t.Errorf, t.Fatal, or comparison against an expected value anywhere. go test reports PASS for this test unconditionally, whether ParseConfig returns a correct struct, an empty one, or silently drops the timeout field entirely. This compiles cleanly and “looks like a test” — which is exactly why it is dangerous: a real bug introduced later will not turn this test red, because nothing in it can ever fail. The fix is a real assertion: want := Config{Timeout: 30 * time.Second}; if cfg != want { t.Errorf("ParseConfig() = %+v, want %+v", cfg, want) }.