A test double is a substitute for a dependency. It can make a test deterministic, but every substitute also makes the test less representative of the real system. The useful question is not “how can every dependency be mocked?” but “which boundary needs control for this test?”
Four common kinds #
- Stub: returns a prepared value, such as a fixed API response.
- Mock: records or constrains interactions, such as whether an email notification was requested.
- Fake: a lightweight working implementation, such as an in-memory repository.
- Spy: wraps a real function while recording how it was called.
Libraries use these terms differently, but the purposes help decide what a test actually needs.
Substitute an external boundary #
For example, a notification service might accept a sender dependency:
export async function inviteMember(email, sendEmail) {
if (!email.includes("@")) throw new Error("Invalid email");
await sendEmail({ to: email, template: "invite" });
return { invited: true };
}
A unit test can pass a simple function that records the request. This checks that valid input triggers the expected notification without sending real email. The invalid-email test should also prove that no message is sent.
Prefer fakes when state matters #
An in-memory fake repository can make it possible to test behavior such as “an invitation is only created once” without mocking every method call. Keep its behavior close to the production contract, and add integration coverage for the real database adapter.
Avoid over-mocking #
Tests that assert every internal call can fail after harmless refactoring. Worse, they can pass even if real collaborators no longer agree on data shapes. Mock only where you need a controllable boundary or a side effect, and test important component wiring with real or faithful collaborators.
A quick choice #
- Need a fixed answer? Use a stub.
- Need a small stateful substitute? Use a fake.
- Need to verify a significant side effect? Use a mock.
- Need to observe a real function? Use a spy.
Choose the simplest double that gives the test control without undermining the behavior it should prove.