Week 20 - Testing Strategy: Five Surfaces, Race-Clean¶
20.1 Conceptual Core¶
- A production Go service has five test surfaces:
- Unit-
*_test.goin the same package, table-driven, fast. - Integration-
*_test.gowith a real Postgres/Kafka/Redis viatestcontainers-go. - Property-based-
gopteror stdlibtesting/quick. Less common in Go than in Haskell/Rust, but valuable for parsers and serializers. - Fuzz-stdlib
func FuzzX(f *testing.F)(since Go 1.18). Native, well-integrated, must be in CI. - End-to-end-the binary, with all real dependencies, via
go runor compiled artifact. - Each surface answers a different question. Skipping one leaves a class of bugs uncovered.
20.2 Mechanical Detail¶
- Table-driven test idiom:
func TestParse(t *testing.T) { tests := []struct{ name string in string want Result err error }{ {"empty", "", Result{}, ErrEmpty}, // ... } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := Parse(tc.in) if !errors.Is(err, tc.err) { t.Fatalf("err: got %v, want %v", err, tc.err) } if !cmp.Equal(got, tc.want) { t.Fatalf("got %v, want %v", got, tc.want) } }) } } testify/requirefor terse assertions,google/go-cmp/cmpfor deep equality with custom comparers.- Fuzz tests:
Run as
func FuzzParse(f *testing.F) { f.Add("hello") f.Fuzz(func(t *testing.T, in string) { out, err := Parse(in) if err == nil { if Roundtrip(out) != in { t.Fatal("not idempotent") } } }) }go test -fuzz=FuzzParse -fuzztime=30s. Persist the corpus. testcontainers-gofor integration: spin a real Postgres in-test, get a connection string, run schema migrations, exercise the adapter. Per-test cost is ~1–3 s container startup; amortize via test-package-level setup.- Race detector economics: - race` slows tests 5–10× and uses ~5–10× memory. Always run in CI; locally optional. Always run on a freshly written concurrent test before committing.
20.3 Lab-"Test-Pyramid the URL Shortener"¶
- Unit: 100% line coverage on
internal/domainandinternal/applicationusing mocks for ports. - Integration:
testcontainers-goPostgres for the postgres adapter. - Fuzz: fuzz the alias-generation function, persisting any crashing inputs.
- Property:
goptertest that "shorten then resolve returns original URL." - E2E: a
make e2etarget that spins the full stack viadocker-compose, hits the HTTP API, asserts behavior. - All five surfaces run in CI under - race -count=1`.
20.4 Idiomatic & golangci-lint Drill¶
tparallel,paralleltest(encouragest.Parallel()),thelper(mark test helpers),testifylint(correct testify usage).
20.5 Production Hardening Slice¶
- Add a continuous-fuzzing job (e.g., scheduled GitHub Action) that runs each
Fuzz*function for 5 minutes against the latest corpus. Persist the corpus as an artifact. Any new crashing input is a P0 issue with a runbook entry.
Month 5 Capstone Deliverable¶
A production-shaped url-shortener-prod/ workspace with:
- Hexagonal layout enforced by depguard.
- Full observability stack (slog + Prometheus + OTel + pprof).
- gRPC sibling service (e.g., a metrics-export gRPC) with hardened client/server config.
- Five test surfaces in CI, all - race - clean.
- A one-page RUNBOOK.md describing alarms, dashboards, deadline budgets, and rollback procedures.
This is the first artifact that resembles a real production service. Treat it as a portfolio piece.