Lesson 21 · Testing, tooling & performance
Mocks & interfaces in tests
Why Part 2's small interfaces pay off — testing with no database.
Your win: wire a testify mock for a repository interface the way our repo does, and explain why the interface seam (Lesson 6) is what makes it possible.
Interfaces are what make mocking work
Recall Lesson 6: our handlers depend on small, consumer-side interfaces, not concrete repos. That's the whole trick — in a test you inject a mock that satisfies the same interface, so the handler runs its real logic against a fake dependency. No interface, no seam, no mock.
testify mocks, the repo way
Our mocks embed testify/mock.Mock and are generated by a custom tool
(cmd/utils/mock/genSpike.go) — regenerate with
make service=spike gen-mock-service after any interface change:
type MockEmailRepo struct{ mock.Mock }
func (r *MockEmailRepo) UpsertEmail(ctx, db, e *model.Email) error {
args := r.Called(ctx, db, e); return args.Error(0)
}
In the test you program the mock, run the code, then verify the calls happened:
internal/spike/.../commands/create_email_handler_test.godb := new(mock_database.Ext); tx := new(mock_database.Tx)
repo := new(mock_repo.MockEmailRepo)
db.On("Begin", ctx).Return(tx, nil) // expect + result
tx.On("Commit", ctx).Return(nil)
repo.On("UpsertEmail", ctx, mock.Anything,
mock.MatchedBy(func(e interface{}) bool { ... })). // assert on the arg
Once().Return(nil)
err := handler.CreateEmail(ctx, payload) // act
require.NoError(t, err)
mock.AssertExpectationsForObjects(t, db, tx, repo) // verify all fired
.On("Method", args).Return(vals) — and
mock.Anything / mock.MatchedBy to match arguments loosely or
precisely. Act: call the code. Assert:
AssertExpectationsForObjects fails the test if an expected call never
happened.
mock_database.Ext / mock_database.Tx mock the pgx executor
seam (Lesson 6's database.Ext/QueryExecer), so a handler test
exercises the full transaction flow — Begin → repo calls → Commit
— entirely in memory. That's why our command-handler tests need no running Postgres.
Learn Go with Tests — Mocking + testify/mock
How and why to mock (and when not to). The testify docs are the reference
for .On/.Return/MatchedBy.
Check yourself (from memory)
Q1. Mocking a dependency in Go tests relies on…
Q2. repo.On("UpsertEmail", ...).Return(nil) sets up…
mock.Anything/MatchedBy control argument matching.
Q3. mock.AssertExpectationsForObjects(t, repo) checks that…
database.Ext/Tx) — Lesson 6. A test injects testify mocks
(MockEmailRepo, mock_database.Ext) via the constructor, programs
them with .On(...).Return(...), runs the handler, and verifies with
AssertExpectationsForObjects. No real DB needed because the interface seam
lets you substitute a fake. Interfaces → testability.