Lesson 9 · Testing the layers & running
Testing a usecase/handler
The flagship pattern: inject mock repositories into the handler, drive its transaction, and verify the entity it built.
Your win: write a usecase test end to end — construct and inject the mocks, mock
the transaction, assert on the entity via MatchedBy, and verify — using spike's
CreateEmail as the template.
The strategy: mock the layer below
A usecase's job is business logic — it orchestrates repositories inside a transaction. So you test it by mocking the repositories and the database, then checking it called them correctly. Everything you learned in Part 2 comes together here.
Arrange — build and inject the mocks
mockDB := new(mock_database.Ext) // the pool
mockTx := new(mock_database.Tx) // the transaction
mockEmailRepo := new(mock_repo.MockEmailRepo) // the ports…
mockEmailRecipientRepo := new(mock_repo.MockEmailRecipientRepo)
mockEmailMetrics := new(mock_metrics.EmailMetrics)
handler := CreateEmailHandler{ // inject them into the SUT's fields
DB: mockDB, EmailMetrics: mockEmailMetrics,
EmailRepo: mockEmailRepo, EmailRecipientRepo: mockEmailRecipientRepo,
}
This is the seam from Lesson 5 in action: because those fields are interfaces, the mocks
drop right in. (In a full test this goes in the newSuite() factory, Lesson 3.)
Arrange — program the mocks
The handler opens a transaction, upserts two entities, and records a metric. Program each:
mockDB.On("Begin", ctx).Return(mockTx, nil) // the transaction opens…
mockTx.On("Commit", ctx).Return(nil) // …and commits
mockEmailRepo.On("UpsertEmail", ctx, mockTx, mock.MatchedBy(func(e interface{}) bool {
email, ok := e.(*model.Email) // ← assert the handler built it right:
return ok && email.Subject.String == "subject"
&& email.EmailID.String != "" // it generated a ULID
&& email.Status.String == "EMAIL_STATUS_CREATED" // it set the status
})).Return(nil)
mockEmailMetrics.On("RecordEmailEvents", ...).Once() // the metric fires exactly once
UpsertEmail.
mock.MatchedBy is where that behaviour is asserted. The mock isn't just a stand-in
here; its matcher is the test's core assertion. And mockTx gives you the
transaction: to test the rollback path, make a repo call return an error and check the handler
surfaces it (checklist case 6).
Act & assert
email, err := handler.CreateEmail(ctx, createEmailPayload) // act — one call
assert.Nil(t, err) // assert the result…
assert.NotEmpty(t, email.EmailID)
mock.AssertExpectationsForObjects(t, mockDB, mockTx, mockEmailRepo, mockEmailRecipientRepo, mockEmailMetrics) // …and the behaviour
The full usecase test
Open the real file and read it top to bottom — it's the template you'll copy most.
→ in-repo internal/spike/modules/email/application/commands/create_email_handler_test.go
→ testify/mock (the API it uses) ·
rules .claude/rules/go-test-style.md
Check yourself (from memory)
Q1. How is a usecase test's system-under-test given its collaborators?
CreateEmailHandler{DB: mockDB, EmailRepo: mockEmailRepo, …}
— the interface fields accept the mocks directly.
Q2. Why is mock.MatchedBy the core assertion in this test?
UpsertEmail.
Q3. What's the flaw in this flagship test file?
mock_database.Ext/Tx,
mock repos, mock metrics) and inject into the handler's interface fields
(CreateEmailHandler{DB: mockDB, EmailRepo: mockEmailRepo, …}); program them —
mockDB.On("Begin").Return(mockTx), mockTx.On("Commit").Return(nil),
mockEmailRepo.On("UpsertEmail", ctx, mockTx, mock.MatchedBy(…entity fields…)).Return(nil),
metric .Once(). Act: handler.CreateEmail(ctx, payload).
Assert: result (assert.Nil(err), NotEmpty(id)) +
AssertExpectationsForObjects. MatchedBy is the core assertion (the entity
isn't returned). Caveat: the real file is happy-case-only — write all six checklist cases yourself.Begin/Commit, and drive it. Since it hands
entities to the repo rather than returning them, I assert on those arguments with
MatchedBy, then verify all the expected calls happened. And I cover every error path —
a green happy-case test isn't a finished one."
testutil.MockDB, no
database in sight. Ask me to sketch the rollback-path case for this handler if
you'd like to see the sixth checklist item.
1. In-repo: …/create_email_handler_test.go:26-164. testify/mock.