Lesson 6 · Mocks & mock generation
testify/mock in practice
Program a mock to expect a call, match its arguments, and verify it happened — the four moves you'll make in every test.
Your win: read a generated mock, program it with On/Return, match
arguments by value / wildcard / predicate, and verify the whole thing with one call.
What a generated mock actually is
Before programming one, see what you're holding. A generated mock embeds testify's
mock.Mock and each method just forwards to r.Called(...):
type MockEmailRepo struct { mock.Mock }
func (r *MockEmailRepo) UpsertEmail(arg1 context.Context, arg2 database.QueryExecer, arg3 *model.Email) error {
args := r.Called(arg1, arg2, arg3) // record the call, look up the programmed response
return args.Error(0) // return what you told it to
}
func (r *MockEmailRepo) Get(arg1 context.Context, arg2 database.QueryExecer, arg3 []string) (model.Emails, error) {
args := r.Called(arg1, arg2, arg3)
return args.Get(0).(model.Emails), args.Error(1) // typed value + error
}
r.Called(...) is the heart of it: it records that the method was called with these
args, finds the matching .On(...) you programmed, and hands back the values from
.Return(...) via args.Get(i) / args.Error(i). (You never write
this file — Lesson 7 generates it. But knowing the shape demystifies the API.)
Move 1 — program: .On(...).Return(...)
Tell the mock what to expect and what to give back:
mockDB.On("Begin", ctx).Return(mockTx, nil) // when Begin(ctx) is called → return (mockTx, nil)
mockTx.On("Commit", ctx).Return(nil) // when Commit(ctx) is called → return nil
Add .Once() to assert it's called exactly once (metric calls in the same test use it,
:149). The method name is a string — a typo compiles but fails at runtime with
"mock: I don't know what to return," so match it to the interface exactly.
Move 2 — match arguments
Each argument in .On is matched against the real call. Three ways to specify one:
| Matcher | Means | Use when |
|---|---|---|
a literal (ctx, an id) | must equal exactly | the value is known and matters |
mock.Anything / mock.AnythingOfType("string") | any value / any of that type | the value doesn't matter to the case |
mock.MatchedBy(func(x) bool) | passes a predicate on the value | you need to assert the argument's contents |
mock.MatchedBy is the powerful one — it lets an expectation check the fields of a
complex argument. spike uses it to verify the entity the handler built:
mockEmailRepo.On("UpsertEmail", ctx, mockTx, mock.MatchedBy(func(e interface{}) bool {
email, ok := e.(*model.Email)
return ok &&
email.Subject.String == "subject" && // the handler mapped the subject
email.EmailID.String != "" && // it generated a ULID
email.Status.String == "EMAIL_STATUS_CREATED" // it set the status
})).Return(nil)
mock.MatchedBy is
that inspection point, folded into the expectation. Its predicate is your assertion about
the handler's behaviour.
Move 3 — verify: AssertExpectationsForObjects
mock.AssertExpectationsForObjects(t, mockDB, mockTx, mockEmailRepo, mockEmailRecipientRepo, mockEmailMetrics)
This fails the test if any programmed .On was never called — the behaviour
check that makes these true mocks (Lesson 5). It's the assertExpected helper from Lesson
3. Forget it, and a mock that's never exercised passes silently — a common way tests give false
confidence.
Move 4 (bonus) — inspect: .Run
When you need to do something when a mock is called — capture an argument, or mutate an
output parameter — chain .Run(func(args mock.Arguments){ … }) before
.Return. This is exactly how the DB toolkit fakes Scan (Lesson 8), so it's
worth recognizing now.
testify/mock — the API
The full surface: On, Return, Once, the argument
matchers, and the assert helpers.
→ pkg.go.dev — testify/mock
→ testify README — mock ·
in-repo …/create_email_handler_test.go:80-164
Check yourself (from memory)
Q1. What does mockDB.On("Begin", ctx).Return(mockTx, nil) set up?
.On(...).Return(...) programs an expectation — the
response for a future matching call. It doesn't invoke anything.
Q2. When do you reach for mock.MatchedBy?
Q3. What does AssertExpectationsForObjects fail on?
assert.* calls.
type MockX struct{ mock.Mock }; each
method does args := r.Called(...) → args.Get(i).(T)/args.Error(i).
1. Program: m.On("Method", args…).Return(vals…).Once() — method name
is a string (typo → runtime fail). 2. Match args: literal (exact) ·
mock.Anything/AnythingOfType (wildcard) · mock.MatchedBy(func(x)
bool) (assert on contents — e.g. the built entity's Subject/EmailID/Status). 3.
Verify: mock.AssertExpectationsForObjects(t, m1, m2…) fails if any programmed
.On never happened (behaviour verification; = assertExpected).
4. Inspect: .Run(func(args mock.Arguments){…}) before
.Return to capture/mutate args (how the DB toolkit fakes Scan)..On(method,
args).Return(vals), match arguments by value, wildcard, or a MatchedBy predicate
that inspects the argument's contents, then verify with AssertExpectationsForObjects —
which fails if a programmed call never happened. That last part is what makes them mocks, not
stubs."
make gen-mock, and the two generators behind mock/. Ask me
to expand any of the four moves.
1. testify/mock. In-repo: mock/spike/…/repositories/email_repo.go:13-25, …/create_email_handler_test.go:80-164.