Lesson 5 · Mocks & mock generation
Test doubles & why mock
The family of stand-ins, the one distinction that matters (mock vs stub), and the interface seam that makes a struct testable at all.
Your win: name the kinds of test double, explain how a mock differs from a stub, and point at the exact code seam that lets you drop a mock in where a real repository would be.
The family: test doubles
"Test double" is Martin Fowler's umbrella term for any pretend object you put in place of a real one during a test.1 There are five, and the distinction is worth knowing precisely — interviewers ask:
| Double | What it does |
|---|---|
| Dummy | passed around but never used (fills a parameter slot) |
| Stub | returns canned answers to calls — state setup |
| Spy | a stub that also records how it was called |
| Mock | pre-programmed with expectations it will verify |
| Fake | a real, lightweight working implementation (e.g. an in-memory DB) |
The one distinction: mock vs stub
This repo's doubles are true mocks. When you write
mockRepo.On("UpsertEmail", …).Return(nil) and then
mock.AssertExpectationsForObjects(t, mockRepo), you're not just feeding data back —
you're asserting the code called UpsertEmail as promised. If it never did, the
test fails. That's the mock's defining behaviour (Lesson 3's assertExpected).
Why mock at all?
Isolation & speed
No real DB, network, or Docker — the test runs in milliseconds and can't be flaky because of something outside the unit.
Reach the unreachable paths
How do you test "the repo returns an error"? With a real DB, you can't easily make it
error. With a mock, .Return(nil, errFoo) — trivial. Mocks are how you cover the
error-path checklist (Lesson 4).
The seam: why a mock can go where a repo goes
You can only substitute a mock if the code depends on an interface, not a concrete type. This is the payoff of the port/adapter design (if you took the Go-techstack or Auth courses, this is the same seam). Look at spike's handler — its repo fields are interfaces:
type CreateEmailHandler struct {
DB database.Ext // an interface
EmailMetrics metrics.EmailMetrics // an interface
EmailRepo infrastructure.EmailRepo // ← a PORT (interface), not repositories.EmailRepo
EmailRecipientRepo infrastructure.EmailRecipientRepo
}
Because EmailRepo is the interface infrastructure.EmailRepo, the test can
set it to a *mock_repositories.MockEmailRepo and the handler can't tell the difference.
In production it's the real pgx adapter; in the test it's a mock. "Accept interfaces" is what
makes code unit-testable — no interface, no seam, no mock.2
Mocks Aren't Stubs (Martin Fowler)
The canonical piece on test doubles — the mock/stub distinction, and behaviour vs state verification.
→ martinfowler.com — Mocks Aren't Stubs ·
Test Double (bliki)
→ go.dev — accept interfaces (the seam) ·
in-repo …/create_email_handler.go:19-25
Check yourself (from memory)
Q1. What distinguishes a mock from a stub?
AssertExpectations); a stub only answers queries.
Q2. Why is mocking the easiest way to cover a repo's error path?
.Return(nil, errFoo) — trivial.
Q3. What lets a test substitute a mock for the real repository?
EmailRepo is
infrastructure.EmailRepo (an interface), so a mock implementing it fits.
AssertExpectationsForObjects
fails if a programmed call never happened. Why mock: isolation/speed +
determinism + reach error paths (.Return(nil, errFoo)). The seam: a
handler field typed as an interface (EmailRepo infrastructure.EmailRepo) can
be set to a mock — "accept interfaces" is what makes code unit-testable; hard-to-test often = bad
design.On/Return, match arguments, and verify. Ask me to walk the
double family again if the mock/stub line is slippery.
1. Fowler — Mocks Aren't Stubs, Test Double.
2. go.dev — interfaces. In-repo: …/application/commands/create_email_handler.go:19-25.