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(...):

mock/spike/modules/email/infrastructure/repositories/email_repo.go:13-25
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:

internal/spike/modules/email/application/commands/create_email_handler_test.go:80-81
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:

MatcherMeansUse when
a literal (ctx, an id)must equal exactlythe value is known and matters
mock.Anything / mock.AnythingOfType("string")any value / any of that typethe value doesn't matter to the case
mock.MatchedBy(func(x) bool)passes a predicate on the valueyou 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:

create_email_handler_test.go:82-101 (condensed)
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)
This is how you assert on a "command" The handler doesn't return the entity — it hands it to the repo. So the only way to check the handler built it correctly is to inspect the argument it passed. mock.MatchedBy is that inspection point, folded into the expectation. Its predicate is your assertion about the handler's behaviour.

Move 3 — verify: AssertExpectationsForObjects

create_email_handler_test.go:164
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.

Read this next

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?

Its predicate inspects the argument's fields — the way to check a "command" the code passes but doesn't return.

Q3. What does AssertExpectationsForObjects fail on?

It verifies every programmed expectation was met — behaviour verification. Value checks are separate assert.* calls.
Recall: the four testify/mock moves.
shape + program + match + verify, then reveal
The mock: 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).
🎯 Interview one-liner "How do you use testify mocks?" → "Program expectations with .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."
Next: where these mocks come fromgenerating mocks with 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.