Unit tests
Inject dependencies
Pass repositories, clocks, and ID generators into code so tests can control behavior without patching module internals.
Code is hard to test when it builds its own dependencies. A function that imports the database client, calls new Date(), and generates a UUID inside its body leaves you nothing to control.
Dependency injection sounds like a big framework thing. It isn’t. In JavaScript it can be a function parameter.
Pass the dependencies in
Let’s rewrite book creation so it receives what it needs. A factory function takes the dependencies and returns the real createBook():
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
}
}
In production, you call makeCreateBook() once, near the entry point, with the real SQLite repository, { now: () => new Date() }, and crypto.randomUUID. In a test, you pass small deterministic substitutes:
const inserted = []
const createBook = makeCreateBook({
repository: { insert: async book => inserted.push(book) },
clock: { now: () => new Date('2026-01-15T10:00:00Z') },
createId: () => 'book-123'
})
Notice how little the test repository needs. Just insert(). The handler depends on that one method, not on a whole database client. That narrow contract makes the real dependency obvious, and it stops test code from growing into a fake database.
Where to inject
Injection turns into noise when every pure helper gets passed down through five layers. Use it at the boundaries: storage, time, IDs, email, network calls. Anything slow, random, or outside the process.
Domain rules like normalizeBook() stay ordinary functions. They don’t need injecting because they have nothing to control.
Test the failure path too
A substitute that always succeeds proves the happy path and nothing else. Make insert() throw the same application-level error the real repository throws, a DuplicateIsbnError for instance, and assert what the caller sees.
One warning. Don’t assert the order of private calls unless that order is itself a requirement. Otherwise a harmless refactor breaks the test and you spend an hour fixing a test for code that still works.
Try this: change createBook() to receive { repository, clock, createId } and test it with in-memory substitutes. Cover one success and one repository failure. Then check that the production wiring still passes the real implementations, because a factory nobody calls correctly is a bug too.
Lesson completed