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

PackageOn failureUse it in…
assert.*marks the test failed, keeps goingtest bodies (almost everywhere)
require.*marks failed and stops immediately (FailNow)helpers, where continuing is pointless
The house convention Tests here use 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):

the vocabulary
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):

1 error from every external call (repo / DB / downstream returns an error) 2 nil / not-found from repos (the "empty" path) 3 guard / early-return conditions (the "already attached", "empty input" branches) 4 happy path — successful create 5 happy path — successful update 6 transaction failures (Begin / Commit / Rollback fails)
This is how you reach ">80%" The soft target is >80% case coverage per function — and it's the checklist, not a coverage tool, that gets you there. Walk each external call and ask "what if this errors?" (case 1); walk each branch (cases 2–3); then the happy paths (4–5) and the transaction (6). Most under-tested functions are missing case 1 (error paths) — the flagship spike handler test, for instance, has only the happy case, so it's paired with the gRPC test that does cover the errors (you'll see both in Part 3).
Coverage the number, vs coverage the goal 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.)
Read this next

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?

A generated ULID is unpredictable, so assert it's non-empty. Exact equality would be brittle or impossible.

Q3. Which is the most commonly-missed case on the coverage checklist?

Error paths (case 1) are the usual gap — the flagship handler test even has only the happy case, covered by a sibling test.
Recall: assert vs require + the six-case checklist.
the rule + the vocab + six cases, then reveal
assert vs require: 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).
🎯 Interview one-liner "How do you know a function is well-tested?" → "A checklist, not a percentage: an error case for every external call, the nil/not-found path, each guard branch, the happy create/update, and transaction failures. Coverage the number is a smoke alarm — 100% lines with no error-path cases still misses bugs — so I chase behaviours and glance at the number."
🏁 Part 1 complete — the foundation You can now write the skeleton of any test here: a table of named subtests, parallel and timed, each with a fresh 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.
Part 2 opens mocks & mock generation — test doubles and why we mock, testify in practice, the two generators behind 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.

2. go.dev — The cover story.