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:

DoubleWhat it does
Dummypassed around but never used (fills a parameter slot)
Stubreturns canned answers to calls — state setup
Spya stub that also records how it was called
Mockpre-programmed with expectations it will verify
Fakea real, lightweight working implementation (e.g. an in-memory DB)

The one distinction: mock vs stub

Fowler's line, in one sentence A stub answers queries (you set it up to return data, then check the result). A mock verifies commands (you tell it what calls to expect, then it asserts those calls happened). Stubs do state verification; mocks do behaviour verification.1

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:

internal/spike/modules/email/application/commands/create_email_handler.go:19-25
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

The design/test feedback loop If a function is hard to unit-test, it's usually telling you something about the design: a concrete dependency that should be an interface, or too many responsibilities in one place. Testability and good decomposition are the same property viewed from two sides.
Read this next

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?

Behaviour vs state: a mock asserts the expected calls occurred (AssertExpectations); a stub only answers queries.

Q2. Why is mocking the easiest way to cover a repo's error path?

A real dependency is hard to force into an error; a mock does it with .Return(nil, errFoo) — trivial.

Q3. What lets a test substitute a mock for the real repository?

The port seam: EmailRepo is infrastructure.EmailRepo (an interface), so a mock implementing it fits.
Recall: the doubles, mock vs stub, and the seam.
the family + the distinction + why testable, then reveal
Test doubles (Fowler): dummy (unused filler), stub (canned answers), spy (stub + records calls), mock (pre-programmed expectations it verifies), fake (lightweight real impl). Mock vs stub: stub answers queries (state verification); mock verifies commands (behaviour verification). This repo's are true mocksAssertExpectationsForObjects 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.
🎯 Interview one-liner "Mock or stub?" → "A stub answers queries with canned data — state verification. A mock is pre-programmed with the calls it expects and asserts they happened — behaviour verification. We use true mocks; the test fails if the code didn't call a collaborator it was supposed to. And you can only swap either in because the code depends on interfaces, not concrete types."
Next: the mechanics — testify/mock in practice: how to program a mock with 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.