Tests become slow and fragile when every business rule is reached through an HTTP handler. To test whether an expired invitation is rejected, I should not need an API Gateway event, a JWT, environment variables, and a running MongoDB instance.
The useful seam is the repository port used by the application:
interface TaskRepository {
findById(id: string): Promise<Task | null>;
save(task: Task): Promise<void>;
}
The use case receives that dependency:
export const completeTask =
(tasks: TaskRepository) => async (taskId: string, userId: string) => {
const task = await tasks.findById(taskId);
if (!task) throw new TaskNotFoundError();
if (task.ownerId !== userId) throw new ForbiddenError();
if (task.completedAt) return;
await tasks.save({ ...task, completedAt: new Date() });
};
Test the decision, not the framework#
With Jest, the test supplies a tiny implementation of the port:
test("does not save a task owned by someone else", async () => {
const tasks: TaskRepository = {
findById: jest.fn().mockResolvedValue({
id: "task-1",
ownerId: "user-a",
completedAt: null,
}),
save: jest.fn(),
};
await expect(completeTask(tasks)("task-1", "user-b")).rejects.toBeInstanceOf(
ForbiddenError,
);
expect(tasks.save).not.toHaveBeenCalled();
});
This test is fast because it contains no I/O. More importantly, a failure points at the rule rather than test setup.
Use the right test at each boundary#
Unit tests cover application decisions: permissions, transitions, idempotent no-ops, and error cases. Repository integration tests cover MongoDB behavior such as indexes, filters, and atomic updates. A smaller number of handler tests cover request parsing and response mapping.
Avoid asserting every internal call. The valuable assertion above is that an unauthorized action rejects and nothing is saved. If I later refactor the use case into smaller functions, that behavior still matters.
Mocks can also lie. An in-memory repository will not reproduce unique-index violations or MongoDB update semantics. That is why adapters deserve focused integration tests against the real technology they wrap.
The goal is not to eliminate integration tests. It is to place most tests around deterministic business behavior and reserve slower infrastructure tests for the behavior only infrastructure can prove.