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:

usecase test → mock database.Ext / Tx (Begin / Commit / Rollback) ← the transaction repository test → mock the pgx layer via testutil.MockDB (Exec / Query / Scan) ← the driver

Level 1 — the transaction (usecase test)

You saw this in Lesson 6. The handler opens a transaction and commits; you mock exactly that:

create_email_handler_test.go:26-27, 80-81
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:

mock/testutil/mock_db_wrapper.go:15-32
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":

infrastructure/repositories/email_test.go:225-270 (the Test_Get idiom, condensed)
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:

mock/testutil/mock_db_wrapper.go:89-93 (MockRowScanFields — the core trick)
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.

Two things it fakes, one thing it captures Fakes: the query returns rows, and Scan fills your entity — no Postgres. Captures: MockExecArgs/MockQueryArgs grab the SQL string the repo emitted and run it through a real Postgres parser (ParseSQLpg_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.
Why everything is doubled (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.
Read this next

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.
Recall: mocking the database at both levels.
usecase vs repo + the Scan trick, then reveal
Level 1 (usecase): mock mock_database.Ext + Txdb.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 (ParseSQLpg_query_goRawStmt) so you can assert query shape. v4/v5: everything doubled (Ext/Ext5) — live migration.
🏁 Part 2 complete — mocks & mock generation You can now reason about test doubles (mock vs stub), program and verify a testify mock (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.
🎯 Interview one-liner "How do you unit-test a repository without a database?" → "A house toolkit mocks the pgx driver: it programs 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."
Part 3 puts it all together — a real test at each layer of spike (usecase, repo, gRPC handler), then running and coverage. Tell me "build Part 3" when you're ready, or ask me anything about mocking first. Re-take Part 2's quizzes cold tomorrow.

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.