Unit tests
Inject dependencies
Pass repositories, clocks, and ID generators into code so tests can control behavior without patching module internals.
8 minute lesson
Code is difficult to test when it creates every dependency for itself. Dependency injection can be as simple as a function parameter.
Construct the production handler with the real repository and production clock. Construct the test handler with small deterministic substitutes. Depend on the narrow behavior the handler needs, not an entire database client.
export function makeCreateBook({ repository, clock, createId }) {
return async function createBook(input) {
const book = {
...input,
id: createId(),
createdAt: clock.now().toISOString()
}
await repository.insert(book)
return book
}
}
Production composition happens once, near the application entry point. Tests can supply a repository with only insert(), a clock with only now(), and an ID function. That narrow contract makes the real dependency obvious and prevents test code from imitating an entire database client.
Injection becomes noise when every pure helper is passed through several layers. Use it at boundaries where behavior is slow, nondeterministic, or outside the process: storage, time, IDs, mail, and network calls. Keep domain rules as ordinary functions.
A substitute must also exercise failure behavior. A fake that always succeeds can prove the happy path but cannot prove that a duplicate record is translated correctly. Configure insert() to throw the same application-level error the real repository exposes, then assert the caller’s observable result. Do not assert private call order unless that order is itself required; otherwise a harmless refactor breaks the test.
Change createBook to receive { repository, clock, createId } and test it with in-memory substitutes. Cover one success and one repository failure, then verify that production construction still passes the real implementations.
Lesson completed