Lesson 8 · Mocks & mock generation
Mocking the database
The hardest collaborator to fake — and the house toolkit that fakes pgx so completely a repository needs no real Postgres.
Your win: mock the database at two levels — the transaction interfaces for a
usecase, and the full pgx driver (via testutil.MockDB) for a repository — and explain
the reflection trick that fakes Scan.
Two levels, because the database appears twice
A usecase uses the database through database.Ext/Tx (to run a
transaction); a repository is the thing that talks to pgx. So you mock at two different
depths:
Level 1 — the transaction (usecase test)
You saw this in Lesson 6. The handler opens a transaction and commits; you mock exactly that:
mockDB := new(mock_database.Ext) // the pool interface
mockTx := new(mock_database.Tx) // the transaction interface
mockDB.On("Begin", ctx).Return(mockTx, nil) // db.Begin() → tx
mockTx.On("Commit", ctx).Return(nil) // tx.Commit() → ok
To test the rollback path, return an error from a repo call inside the transaction and
assert the handler surfaces it — the mocked Tx lets you drive commit-vs-rollback at
will (coverage checklist, case 6). No real transaction ever runs.
Level 2 — the driver (repository test)
A repository test can't mock "the repo" — the repo is the system under test. So you mock
what the repo calls: pgx's Exec/Query/Scan. Doing that by hand
is painful (pgx's Rows has many methods), so the house testutil.MockDB
toolkit wraps it:
type MockDB struct {
DB *mock_database.Ext; DB5 *mock_database.Ext5 // pgx v4 + v5 (a live migration)
Rows *mock_database.Rows; Rows5 *mock_database.Rows5
Row *mock_database.Row; RawStmt *RawStmt // the captured, parsed SQL
}
It gives you high-level helpers so a repo test reads at the level of "set up a query that returns these rows":
mockDB := testutil.NewMockDB()
mockDB.MockQueryArgs(t, nil, ctx, mock.Anything, database.TextArray(ids)) // program Query → Rows
mockDB.MockScanArray(nil, fields, [][]interface{}{ email1Vals, email2Vals })// fake TWO rows of Scan
// error cases: pass an err to MockQueryArgs, or pgx.ErrNoRows into the scan
The clever bit: faking Scan with reflection
How can a mock fill in a repo's scan targets without a database? MockScanArray
programs the mocked Scan and uses .Run (Lesson 6's move 4) to
copy the expected values into the caller's destination pointers via reflection:
m.Row.On("Scan", mockArgs...).Once().Run(func(args mock.Arguments) {
for i := range args {
reflect.ValueOf(args[i]).Elem().Set(reflect.ValueOf(values[i]).Elem()) // *dest = value
}
}).Return(err)
When the repo calls rows.Scan(&e.ID, &e.Subject, …), this .Run
fires and writes your prepared values straight into e.ID, e.Subject, etc. —
exactly what a real Scan would do, minus the database.
Scan fills your entity — no
Postgres. Captures: MockExecArgs/MockQueryArgs grab the
SQL string the repo emitted and run it through a real Postgres parser
(ParseSQL → pg_query_go) into RawStmt — so a test can assert
on the shape of the SQL (the right table, the right WHERE) without ever executing
it. That's how you unit-test a repository's query logic.
Ext/Ext5)
You'll see DB/DB5, Rows/Rows5,
MockScanArray/MockScanArray5 everywhere. That's a live
pgx v4 → v5 migration: both driver versions are mocked in parallel, and a test
uses whichever version its repo is on. Same pattern, two stacks.
The DB interfaces & the mocking toolkit
The real interfaces being faked, and the wrapper that fakes them.
→ in-repo internal/golibs/database/db.go (Ext/QueryExecer) ·
mock/testutil/mock_db_wrapper.go · mock/testutil/sql.go
→ pkg.go.dev — pgx (the driver being mocked)
Check yourself (from memory)
Q1. In a usecase test, how do you mock the transaction?
mockDB.On("Begin").Return(mockTx, nil) +
mockTx.On("Commit").Return(nil) — drive commit vs rollback at will.
Q2. How does MockScanArray fill the repo's scan targets without a DB?
reflect.ValueOf(dest).Elem().Set(value) inside a
.Run — it writes into the pointers the repo passed to Scan.
Q3. What does testutil.MockDB capture the SQL string for?
ParseSQL (pg_query_go) turns the emitted SQL into an
AST (RawStmt) so a test can assert the repo built the right query.
mock_database.Ext +
Tx — db.On("Begin").Return(mockTx, nil), tx.On("Commit"/"Rollback")
— to drive the transaction (checklist case 6). Level 2 (repository, the repo IS the SUT):
mock the pgx driver via testutil.MockDB (bundles
Ext/Tx/Rows/Row + RawStmt).
Helpers: MockQueryArgs (program Query→Rows), MockExecArgs,
MockScanArray/MockRowScanFields (fake Scan). The trick:
MockScanArray uses .Run + reflection
(reflect.ValueOf(dest).Elem().Set(value)) to copy expected values into the repo's scan
destinations — no DB. It also captures + parses the SQL (ParseSQL →
pg_query_go → RawStmt) so you can assert query shape.
v4/v5: everything doubled (Ext/Ext5) — live migration.On/Return/MatchedBy/AssertExpectations), regenerate mocks with the two generators,
and fake the database at both the transaction and driver levels. That's everything you inject into
a test. Part 3 assembles it into real tests, layer by layer.
Query/Exec to return canned rows, fakes Scan
by reflecting the expected values into the scan destinations, and even parses the emitted SQL so I
can assert the query shape. No Postgres — just the driver interfaces, mocked."
1. In-repo: mock/testutil/mock_db_wrapper.go:15-93, mock/testutil/sql.go, …/repositories/email_test.go:225-270, internal/golibs/database/db.go:24-36.