Lesson 12 · Testing the layers & running
Running, coverage & the workflow
Run one test, run a whole service, read the coverage — and the edit loop that keeps mocks and tests honest.
Your win: run tests at every granularity, read the coverage number (and know what it hides), understand why tests run three times, and recite the daily edit → regenerate → test loop — then the whole course in one view.
Running — three granularities
# one test function (the tight inner loop while writing)
go test ./internal/spike/modules/email/... -run ^Test_SendEmail$ -v
# a whole service, with coverage (the pre-push check)
make service=spike unit-test-service
That make target expands to (Makefile:104):
go test -count=3 ./internal/spike/... -cover -covermode=count \
-coverprofile=cover.out -coverpkg=./internal/spike/...
go tool cover -func=cover.out # → per-function coverage; the last line is the total
-count=3 — why tests run three times
-count=3 disables test caching and runs the whole suite three times.
A test that passes once but fails on a re-run is flaky — usually order-dependent or sharing
state. This is why the house style is so strict about independence: t.Parallel() plus a
fresh newSuite() per case (Lessons 2–3) exist precisely so the suite survives
-count=3. If your test only passes at -count=1, it has a hidden shared
dependency — fix it, don't ignore it.
Coverage — the number and its blind spot
go test -cover reports the percent of statements the tests executed.1
The house target is a soft >80% case coverage per function — and note it strips
generated code so the number reflects real logic:
… | grep -v "_generated_impl.go" | grep -v ".sql.go" # exclude generated files from the %
But remember Lesson 4: coverage is a smoke alarm, not the goal. The flagship handler test hit its lines at 100% while missing every error path — the number was green and the tests were incomplete. Chase the six-case checklist (behaviours); glance at the percent to catch a whole branch left unrun.
The daily loop — two rules, one habit
Two rules from .claude/rules/go-code-style.md define the workflow:
Rule 1 (regenerate after an interface change) prevents the baffling "mock no longer implements the interface" build break (Lesson 7). Rule 2 (run the nearest test after editing) catches regressions while the change is fresh. Skip either and you pay for it later, further from the cause.
🏛️ The whole course, in one view
-count=3 passes). 2. Mock vs stub — ours verify behaviour, not just
return data. 3. The interface seam — DI is what makes code testable at all.
4. Mock the layer below — repo/usecase/transport each isolate the next rung
down. 5. Coverage is a checklist, not a percentage — error paths are where bugs
hide.
Go coverage + Learn Go with Tests
What the coverage number measures, and a community-standard, TDD-first way to deepen the craft.
→ go.dev — The cover story ·
in-repo Makefile:99-114, .claude/rules/go-code-style.md:41-60
→ Learn Go with Tests (community, for going further)
Check yourself (from memory)
Q1. Why does unit-test-service use -count=3?
Q2. Which is true about the coverage number here?
_generated_impl.go/.sql.go;
and lines-covered ≠ cases-covered — chase the checklist.
Q3. After changing an interface, what must you do before the code compiles?
go test ./…/... -run ^TestX$ -v;
a service — make service=spike unit-test-service (go test -count=3 -cover
-covermode=count -coverprofile -coverpkg → go tool cover -func, total on last
line). -count=3 = run 3× to detect flaky/order-dependent tests → why
everything is parallel-safe with a fresh newSuite(). Coverage: % of
statements executed; soft >80% case/function; generated code stripped
(_generated_impl.go, .sql.go); a smoke alarm, not the goal (100% lines can
miss error paths). Daily loop: change interface → make gen-mock-service
(rule 1) → edit impl → run nearest test (rule 2) → unit-test-service for coverage
before pushing.mockgen-header lie, testutil.MockDB's
reflection Scan, -count=3). That's interview-grade fluency in the repo's test craft.
1. go.dev — The cover story. In-repo: Makefile:99-114, .claude/rules/go-code-style.md:41-60.