Lesson 10 · Testing the layers & running
Testing a repository
The repo is the system under test, so you mock the driver beneath it — a round-trip through testutil.MockDB, no database in sight.
Your win: write a repository test that fakes a query and its rows, gets real
entities back, and covers the success, query-error, and scan-error cases — using spike's
Test_Get as the template.
The shift: mock below the SUT
For a usecase you mocked the repos (Lesson 9). But a repository is the thing that
talks to the database — it's the SUT, so you can't mock it. You mock the layer below it: the
pgx driver, via testutil.MockDB (Lesson 8). The SUT is the real repo:
mockDB := testutil.NewMockDB()
db := mockDB.DB
r := &EmailRepo{} // ← the REAL repo is the SUT; only the driver is mocked
Arrange — fixtures + a faked query and rows
Build the entities you expect back (the house helper database.AllRandomEntity fills
one with random values), then program the query and the scan:
ent1, ent2 := &model.Email{}, &model.Email{}
database.AllRandomEntity(ent1); database.AllRandomEntity(ent2) // random fixtures
ent1.EmailID.Set("email-id-1"); ent2.EmailID.Set("email-id-2")
fields, vals1 := ent1.FieldMap() // column names + scan-target values
_, vals2 := ent2.FieldMap()
mockDB.MockScanArray(nil, fields, [][]interface{}{vals1, vals2}) // fake TWO rows of Scan
mockDB.MockQueryArgs(t, nil, mock.Anything, mock.Anything, database.TextArray(emailIDs)) // fake Query→Rows
FieldMap() gives the entity's column names and the pointers a scan writes into;
MockScanArray then uses reflection (Lesson 8) to copy vals1/vals2
into the repo's scan destinations, one call per row. No Postgres runs.
Act & assert — the round-trip
emails, err := r.Get(ctx, db, emailIDs) // act — the real repo builds SQL, "queries", scans
assert.Nil(t, err)
assert.Equal(t, ent1, emails[0]) // the entity round-tripped back intact
assert.Equal(t, ent2, emails[1])
The repo built a query, "ran" it against the mock, and scanned two rows into
model.Emails — and you got back exactly the entities you seeded. That verifies the repo's
scan wiring (right columns → right fields) without a database.
The error cases — the checklist, cheaply
Two error paths, each one line of setup:
t.Run("error query", func(t *testing.T) {
mockDB.MockQueryArgs(t, pgx.ErrNoRows, mock.Anything, mock.Anything, database.TextArray(emailIDs))
emails, err := r.Get(ctx, db, emailIDs)
assert.Nil(t, emails); assert.ErrorIs(t, err, pgx.ErrNoRows) // Query failed
})
t.Run("error scan", func(t *testing.T) {
mockDB.MockScanFields(pgx.ErrNoRows, fields, vals) // Scan fails
mockDB.MockQueryArgs(t, nil, mock.Anything, mock.Anything, database.TextArray(emailIDs))
emails, err := r.Get(ctx, db, emailIDs)
assert.Nil(t, emails); assert.ErrorIs(t, err, pgx.ErrNoRows)
})
MockQueryArgs → the query fails. Pass an error to the scan → scanning
fails. Two of the six checklist cases, for free, because you control the driver. This is what a
real DB can't easily give you: deterministic failures. Three cases —
success + error-query + error-scan — and the repo's read path is genuinely
covered.1
MockQueryArgs/MockExecArgs captured and parsed the SQL into
mockDB.RawStmt (Lesson 8), a repo test can go further and assert on the query
shape — the right table, a deleted_at IS NULL filter, the expected columns — with
the helpers in mock/testutil/{select,update,where}.go. Useful when the query logic
itself is what you're testing.
The repository test & the DB toolkit
The real file plus the wrapper it leans on — read them side by side.
→ in-repo …/infrastructure/repositories/email_test.go:225-270 ·
mock/testutil/mock_db_wrapper.go
→ pkg.go.dev — pgx ·
strategy .claude/rules/eureka-v2-conventions.md:103-109
Check yourself (from memory)
Q1. In a repository test, what is the system under test?
r := &EmailRepo{} — the real repo. You mock what's
below it (the driver) via testutil.MockDB.
Q2. How do you force a repo test to exercise a scan failure?
err — pass
pgx.ErrNoRows and assert ErrorIs. Deterministic failure, no DB.
Q3. What does the "success" case actually verify about the repo?
r := &EmailRepo{}); mock the
driver below via testutil.NewMockDB(). Arrange (success): build
fixtures (database.AllRandomEntity), get fields, vals := ent.FieldMap(),
mockDB.MockScanArray(nil, fields, [][]interface{}{vals1, vals2}) (fake rows via
reflection) + mockDB.MockQueryArgs(t, nil, …, database.TextArray(ids)). Act:
r.Get(ctx, db, ids). Assert: emails[i] == ent[i] →
verifies scan wiring (columns→fields). Error cases: MockQueryArgs(t,
pgx.ErrNoRows, …) → error-query; MockScanFields(pgx.ErrNoRows, …) → error-scan;
assert.ErrorIs. Three cases cover the read path — no DB. Bonus: assert SQL shape via
mockDB.RawStmt.1. In-repo: …/repositories/email_test.go:225-270, mock/testutil/mock_db_wrapper.go. pgx.