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:
make service=spike gen-mock-service # → go run cmd/utils/main.go mock spike
make gen-mock-v2 # → …mock eureka_v2
.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:
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.
// 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.)
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.
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?
Q2. How does the repo decide which generator makes a given mock?
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…
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.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."
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.