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

.claude/rules/go-test-style.md:18-54 (the mandated shape)
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.

Why inline, not package-level? Defining 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.

A legacy style you'll also see Not every test uses the inline 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.
Read this next

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?

A fresh suite per t.Run — new mocks, wired SUT — so parallel cases stay independent.

Q2. What does assertExpected verify?

It wraps mock.AssertExpectationsForObjects — behaviour verification that the collaborators were called as programmed.

Q3. Why are newSuite/assertExpected defined inside the test function?

Locality + isolation: everything the test needs is in one scope, with no package-level state for a parallel test to corrupt.
Recall: the house test skeleton, piece by piece.
the three pieces + why, then reveal
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).
🎯 Interview one-liner "How do you keep table-driven tests clean?" → "A per-case factory that builds fresh mocks and the system-under-test, an assert-expectations helper to verify the mocks, and strict arrange/act/assert sections. One assertion per behaviour and no branches in a case — so a failing test's name tells you exactly what broke. Everything's local to the test for isolation."
Next: the 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.