Lesson 4 · The unit-test foundation
Assertions & the coverage checklist
How to check a result — and which six cases every function you test should have.
Your win: choose assert vs require correctly, use the
house assertion vocabulary, and enumerate the six-case checklist that turns "I wrote a test" into "I
covered this function."
assert vs require — the one rule
testify gives you two assertion packages with the same API but different failure behaviour:1
| Package | On failure | Use it in… |
|---|---|---|
assert.* | marks the test failed, keeps going | test bodies (almost everywhere) |
require.* | marks failed and stops immediately (FailNow) | helpers, where continuing is pointless |
assert almost everywhere — a failed check records the
problem but lets the rest of the assertions run, so one broken test still tells you everything
that's wrong, not just the first thing. require is reserved for helper
internals (e.g. mock/testutil/sql.go) where a failed precondition means the
rest of the helper would panic anyway. Rule of thumb: require to guard, assert to check.
The house assertion vocabulary
A small, consistent set covers nearly everything (go-test-style.md:66-71):
assert.Equal(t, expected, actual) // values — the workhorse
assert.Nil(t, err) / assert.NotNil // pointer / interface presence
assert.NotEmpty(t, got.ID) // an ID generated INSIDE the function (value unknown)
assert.ErrorIs(t, err, ErrNotFound) // error identity (wrapped-error aware)
assert.Len(t, got, 3) // collection length
assert.NotEmpty is the idiom for values the function creates — a
idutil.ULIDNow() id you can't predict, so you assert it's non-empty rather than equal to
a literal. And when you need to check the arguments a mock received, you inspect them with
.Run(func(args mock.Arguments){ … }) or mock.MatchedBy (Part 2) — not a
separate assertion.
The coverage checklist — six cases per function
"Did I test this function?" has a concrete answer here. The rules list six kinds of case
to cover for each function (go-test-style.md:56-64):
go test -cover reports a percentage of statements executed.2
It's a useful smoke alarm — a function at 30% is clearly under-tested — but 100% line coverage with
no error-path cases is a lie. The checklist targets behaviours; the number just
confirms you didn't leave a whole branch unrun. Chase the checklist, glance at the number. (The
repo even strips generated code from the profile so the number reflects real code — Lesson 12.)
testify assert/require + Go coverage
The two assertion packages, and what the coverage number actually measures.
→ testify/assert ·
testify/require
→ go.dev — The cover story ·
in-repo .claude/rules/go-test-style.md:56-71
Check yourself (from memory)
Q1. When should you use require instead of assert?
require calls FailNow — reserve it for
guards. In test bodies, assert lets every check run.
Q2. How do you assert on an ID the function generated internally?
Q3. Which is the most commonly-missed case on the coverage checklist?
assert.* = non-fatal (keeps
going) → test bodies; require.* = FailNow (stops) → helpers/guards.
"Require to guard, assert to check." Vocabulary: assert.Equal
(values), Nil/NotNil (presence), NotEmpty
(function-generated IDs), ErrorIs (wrapped-error identity), Len. Inspect
mock args with .Run/MatchedBy. Coverage checklist (6 per
function): 1 error from every external call · 2 nil/not-found · 3 guard/early-return · 4
happy create · 5 happy update · 6 tx commit/rollback failure. Soft target >80% case
coverage/function — driven by the checklist, not the tool. go test -cover =
statements executed (a smoke alarm, not the goal); most gaps are missing error paths (case 1).newSuite, arrange/act/assert, the right assertions, and the six-case
checklist in mind. What's missing is the star of the show — the mocks you inject
and program. That's Part 2.
mock/, and how the database gets faked. Tell me
"build Part 2" when you're ready, or ask me anything about Part 1 first. Re-take
these four quizzes tomorrow cold — spacing is what makes it stick.
1. testify/assert, testify/require. In-repo: .claude/rules/go-test-style.md:56-71, mock/testutil/sql.go:14-15.