Lesson 3 · The unit-test foundation
The house structure
A factory for fresh mocks, a helper to verify them, and three comment lines that make every test read the same way.
Your win: write the canonical test skeleton this repo mandates — the
newSuite factory, the assertExpected helper, and the
arrange / act / assert sections — and say why each piece exists.
The skeleton, from the rules
The house style (.claude/rules/go-test-style.md) is precise: a sub-test-per-case with
an inline newSuite factory and an inline
assertExpected helper — both defined inside the test function, not at package
level.1
func TestMyUsecase_Method(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
type testSuite struct {
sut *MyUsecaseImpl
db *mock_database.Ext
tx *mock_database.Tx
mockRepo *mock_postgres.MockMyRepo
}
newSuite := func() *testSuite { /* build fresh mocks + wire the SUT */ }
assertExpected := func(t *testing.T, s *testSuite) {
mock.AssertExpectationsForObjects(t, s.db, s.tx, s.mockRepo)
}
t.Run("return error when ...", func(t *testing.T) {
// arrange
suite := newSuite()
// … program the mocks …
// act
err := suite.sut.Method(ctx, ...)
// assert
assert.Equal(t, expectedErr, err)
assertExpected(t, suite)
})
}
Three pieces, three reasons
newSuite()
A factory that builds a fresh testSuite — new mocks, a wired-up
SUT — per sub-test. Called at the top of every t.Run. This is what makes
parallel cases independent (Lesson 2).
assertExpected()
A one-liner that runs mock.AssertExpectationsForObjects(t, …) — verifying every
programmed mock call actually happened. It's behaviour verification: did the
code call its collaborators as promised?
// arrange / act / assert
Three required comment sections in every sub-test. Arrange: build the suite,
program mocks. Act: call the one method under test. Assert:
check the result + assertExpected.
newSuite/assertExpected inside the test function keeps
each test self-contained — everything it needs is in one scope, with no shared
package-level state that a parallel test could stomp on. You can read (or delete) a test without
hunting through the file for helpers. It's a little repetition bought back as isolation and
locality.
What "one assertion per behaviour" buys you
The rules add two discipline points that keep tests honest: one assertion per behaviour,
and no logic branches inside a case (no if/for in the
sub-test body).1 A case that needs a branch is really two
cases — split it. A case with five unrelated assertions is hiding five behaviours — name and
separate them. The payoff: when a test fails, its name tells you exactly what broke.
newSuite style. Older code (and the
unit-tester agent playbook) uses a package-level TestCase table
— a struct slice with name/req/expectedErr/setup fields. Both are "house style," but
the rules file's inline newSuite version is the canonical one to
write today. Recognize the table style; author the newSuite style.
The house rules, in full
The single source of truth for test structure here — worth reading end to end once.
→ in-repo .claude/rules/go-test-style.md (structure, coverage, assertions, naming)
→ go.dev — Subtests (the t.Run mechanism the skeleton builds on)
Check yourself (from memory)
Q1. What does newSuite() do, and when is it called?
t.Run — new mocks, wired SUT — so
parallel cases stay independent.
Q2. What does assertExpected verify?
mock.AssertExpectationsForObjects —
behaviour verification that the collaborators were called as programmed.
Q3. Why are newSuite/assertExpected defined inside the test function?
func TestX(t): t.Parallel() +
context.WithTimeout(…, 15s), then inline: a testSuite
struct (sut, db *mock_database.Ext, tx *mock_database.Tx,
mockRepo), a newSuite() factory (fresh mocks + wired SUT,
called atop every t.Run → parallel independence), and an
assertExpected(t, s) = mock.AssertExpectationsForObjects(t,
s.db, s.tx, s.mockRepo) (behaviour verification). Each sub-test:
// arrange (suite + program mocks) → // act (call the one method) →
// assert (check result + assertExpected). Rules: one assertion per
behaviour, no branches in a case. Inline (not package-level) = self-contained, no shared state.
Legacy alternative: package-level TestCase table (recognize it; write the
newSuite style).assert half — assert vs require, what to assert,
and the six-case coverage checklist every function should satisfy. Ask me to
sketch a full newSuite for a spike handler if you'd like to see it fleshed out.
1. In-repo: .claude/rules/go-test-style.md:9-54 (structure, one-assertion-per-behaviour, inline helpers). go.dev — Subtests.