Overview
Go's unit-testing story is deliberately minimal: the stdlib testing package provides
*testing.T, subtests, parallelism, cleanup and helpers, and go test compiles and runs
any _test.go file — no framework, no runner to configure. The community's contribution
is idiom rather than machinery: table-driven tests as the universal pattern, hand-rolled
fakes as the default double, and a long-running debate between testify's assertions and
the purist if got != want + go-cmp style.
Key points
*testing.Tis the whole API surface:t.Errorf(fail and continue),t.Fatalf(fail and stop),t.Skip,t.Log— failure is reporting, not exceptions.- Subtests via
t.Run: named, individually addressable (go test -run TestFoo/case), independently reported; the enabling mechanism for table-driven tests. t.Parallel()marks a test (or subtest) safe to run alongside its siblings — cheap wall-clock wins, but shared state and captured loop variables become hazards.t.Cleanup(f)replaces defer-based teardown and composes across helpers;t.Helper()makes a helper's failures report the caller's line, essential for any shared assertion function.t.TempDir()hands out an auto-removed per-test directory;t.Setenvscopes an environment variable to the test (and forbidst.Parallel— env is process-global).- Table-driven tests are THE idiom: cases as data, one loop, one
t.Runper case. Adding a case is adding a struct literal, not a function. - Golden files (
testdata/*.golden) for large or structured expected output, with an-updateflag to regenerate — review the diff in Git, not in your head. - Hand-rolled fakes are the Go default: small consumer-side interfaces make a ten-line in-memory fake cheaper and clearer than a mocking framework.
- Race detector as a habit:
go test -racein CI always; unit tests are where data races are cheapest to find.
Details
Assertions: testify vs go-cmp + plain if
| Style | Stack | Trade-off |
|---|---|---|
| Fluent assertions | stretchr/testify (assert continues, require stops) |
Ubiquitous, readable, rich failure messages; a dependency and a mini-DSL |
| Stdlib purist | if got != want { t.Errorf(...) } + google/go-cmp for deep diffs |
Zero magic, cmp.Diff output is excellent for structs; more boilerplate |
go-cmp earns its place either way: cmp.Diff(want, got) with cmpopts (ignore fields,
tolerate float error, sort slices) beats reflect.DeepEqual's silent false negatives.
Pick one style per repository; mixed suites read worse than either extreme.
Test doubles
- Fake first: define the interface where it is consumed (Go interfaces are satisfied implicitly), then write an in-memory implementation. It serves the whole suite and keeps tests black-box.
- Generated mocks when interactions matter:
uber-go/mock(the maintained gomock fork) generates strict, expectation-based mocks;vektra/mockerygenerates testify-flavoured ones. Reserve them for boundaries where the outgoing call is the behaviour — an interaction-verifying suite couples to call sequence and shatters on refactor. - Time is a dependency: inject a clock (
func() time.Timefield, or a small clock interface) instead of callingtime.Nowin domain logic; sleeping in tests is a flake factory.
Golden files
Store expected output under testdata/ (ignored by the build), compare with cmp.Diff,
and regenerate behind a flag: var update = flag.Bool("update", false, "rewrite golden files"). Works for rendered templates, JSON payloads, CLI output — anything too big to
inline.
Examples
func TestParseDuration(t *testing.T) {
cases := []struct {
name string
in string
want time.Duration
wantErr bool
}{
{name: "seconds", in: "15s", want: 15 * time.Second},
{name: "composite", in: "1h30m", want: 90 * time.Minute},
{name: "garbage", in: "soon", wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := ParseDuration(tc.in)
if tc.wantErr {
if err == nil {
t.Fatalf("ParseDuration(%q): expected error, got %v", tc.in, got)
}
return
}
if err != nil {
t.Fatalf("ParseDuration(%q): %v", tc.in, err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("ParseDuration(%q) mismatch (-want +got):\n%s", tc.in, diff)
}
})
}
}
Related
- Testing in Go — strategies & tooling map — parent map of the strategy portfolio.
- Property-based testing & fuzzing — generated inputs where example tables run out.
- go test — the runner: flags, caching, -run/-race/-cover mechanics.
- TDD — the red/green/refactor discipline the fast loop serves.
- Unit testing in Java — JUnit 5/Mockito/AssertJ as the cross-language contrast.