Unit tests
Use test doubles sparingly
Choose fakes, stubs, spies, and mocks for a precise reason without replacing the behavior the test is supposed to prove.
8 minute lesson
A fake has working simplified behavior, a stub returns a prepared value, and a spy records interaction. “Mock” is often used for all three.
Prefer a small in-memory repository fake when state matters. Use a stub to trigger a rare error and a spy when an interaction is itself the contract. Reset doubles after tests and avoid mocking your own simple pure functions.
The danger is contract drift. If the real repository returns null for a missing book but the fake returns undefined, tests can pass against behavior production never provides. Keep doubles behind a narrow application-owned interface and run separate integration tests against the real adapter.
const repository = {
insert: async () => {
throw new DuplicateIsbnError('9780441172719')
}
}
const result = await createBook(input, { repository })
assert.deepEqual(result, {
ok: false,
code: 'duplicate-isbn'
})
This stub is valuable because it makes a rare failure deterministic. A spy would be appropriate if the contract were “send one confirmation after the transaction commits.” It would be weak evidence for “the book was persisted,” because observing a call does not prove the database accepted it.
Over-mocking creates false positives by copying the implementation into the test: the test expects the exact internal calls it was taught to expect. Prefer asserting returned values, state changes, or public messages. Spy on interactions only at an external boundary where the interaction is the observable outcome.
Test that a storage conflict becomes the documented API conflict response without connecting to a database. Then add one real-database integration test that proves the configured uniqueness constraint produces the error your stub represents.
Lesson completed