Lesson 7 · Mocks & mock generation

Generating mocks

You never hand-write a mock — you generate it. And this repo does it two different ways, on purpose.

Your win: regenerate a service's mocks with one command, and explain the two generators behind mock/ — which one handles structs, which handles interfaces, and the header that lies about both.

The command

Mocks in mock/ are generated, not written. When you add or change an interface, you regenerate:

Makefile:210-214
make service=spike gen-mock-service   # → go run cmd/utils/main.go mock spike
make gen-mock-v2                      # → …mock eureka_v2
The rule that saves you a confusing build break From .claude/rules/go-code-style.md: after adding or updating any interface, run the mock generation. A stale mock (missing a new method, or with an old signature) won't implement the interface anymore — so the code that injects it stops compiling, often with a baffling error far from the real cause. Change interface → regenerate → then run tests.

One command, two generators

Each service registers what to mock in a genXxx.go file. spike's shows the split cleanly — two maps, two generator functions:

cmd/utils/mock/genSpike.go:12-46 (condensed)
structs := map[string][]interface{}{                       // CONCRETE structs
    "…/infrastructure/repositories": {&repositories.EmailRepo{}, &repositories.EmailRecipientRepo{}, …},
    "…/application/commands":         {&commands.CreateEmailHandler{}, …},
}
tools.GenMockStructs(structs)                              // ← generator (a): reflection

interfaces := map[string][]string{                         // NAMED interfaces
    "…/email/metrics":        {"EmailMetrics"},
    "…/email/media_manager": {"MediaDownloader", "MediaInfoFetcher", "IMediaManager"},
}
tools.GenMockInterfaces(interfaces)                        // ← generator (b): mockery

(a) GenMockStructs — reflection codegen

A bespoke generator (internal/golibs/tools/mock_struct.go) that uses reflect to walk a concrete struct's methods and emit type Mock<Name> struct{ mock.Mock } + one r.Called(...) method each (the shape you saw in Lesson 6). Used for repos and handlers.

(b) GenMockInterfaces — mockery wrapper

A thin wrapper over the real mockery v2.46 CLI (mock_interface.go), with --with-expecter — so these mocks also get a type-safe EXPECT() fluent API. Used for named interfaces (metrics, ports).1

Both emit testify mocks into mock/, which mirrors internal/ one-to-one (package mock_<pkg>). So the output is uniform; only the tech differs by whether you pointed at a struct or an interface name.

⚠️ The header that lies The reflection-generated files carry // Code generated by mockgen. DO NOT EDIT. — but it is NOT gomock/mockgen. There is no gomock in this repo at all. The header is a leftover string in the bespoke generator; the file imports stretchr/testify/mock and is pure testify. If you grep for "mockgen" expecting uber's tool, you'll be misled. (A great "did you actually read the code?" interview detail.)
Why two generators at all? Historical/pragmatic: the reflection generator predates the repo's adoption of mockery and still handles concrete structs (where you want a mock of a struct's method set); mockery handles named interfaces (and gives the nicer EXPECT() API). You don't choose — the genXxx.go author already decided by putting a type in the structs map vs naming it in the interfaces map. You just run make gen-mock-service.
Read this next

mockery — interface mock generation

The tool GenMockInterfaces wraps: how it generates testify mocks and the --with-expecter EXPECT() API.

vektra.github.io/mockery · github.com/vektra/mockery
→ in-repo cmd/utils/mock/genSpike.go, internal/golibs/tools/mock_struct.go · rule .claude/rules/go-code-style.md:41-49

Check yourself (from memory)

Q1. When must you regenerate mocks?

A changed interface makes the old mock stop implementing it — the build breaks. Regenerate right after the interface change.

Q2. How does the repo decide which generator makes a given mock?

The genXxx.go author puts concrete structs in the structs map (→ GenMockStructs) and interface names in the interfaces map (→ mockery).

Q3. The // Code generated by mockgen header means…

A misleading leftover string in the bespoke reflection generator. There's no gomock anywhere; it imports testify.
Recall: how mocks are generated + the two generators.
command + the split + the gotcha, then reveal
Command: make service=spike gen-mock-service (or make gen-mock-v2) → runs cmd/utils/main.go mock <svc>. Rule: regenerate after any interface change, or the stale mock stops implementing the interface and the build breaks. Registration (genXxx.go): a structs map (concrete repos/handlers → GenMockStructs, a bespoke reflection codegen, mock_struct.go) and an interfaces map (named ports → GenMockInterfaces, a mockery v2.46 wrapper with --with-expecter). Both emit testify mocks into mock/ (mirrors internal/, pkg mock_<pkg>). Gotcha: the reflection files say // Code generated by mockgen but it's NOT gomock — pure testify, zero gomock in the repo.
🎯 Interview one-liner "How are your mocks generated?" → "A custom gen-mock CLI. Concrete structs go through a bespoke reflection generator; named interfaces through mockery. Both emit testify mocks into a mock/ tree that mirrors the source, and you regenerate after any interface change. One quirk: the reflection mocks carry a mockgen header, but there's no gomock anywhere — it's all testify."
Last lesson of Part 2: the hardest thing to mock — the database. How mock_database.Ext/Tx and the testutil.MockDB toolkit fake pgx without a real Postgres. Ask me if the two-generator split needs another pass.

1. mockery. In-repo: cmd/utils/mock/genSpike.go:12-46, internal/golibs/tools/mock_struct.go + mock_interface.go; rule .claude/rules/go-code-style.md:41-49.