Mocking Strategy
On this page
What Mocking Is For
Mocking replaces a real dependency with a controlled fake so tests are fast and deterministic. This is valuable for external services (payments, email, SMS), time-dependent logic, and failure simulations that are hard to reproduce reliably.
Mock at the Boundary
Mock your service clients and repositories, not your internal functions. If you mock internal implementation details, your tests will pass even when the behavior breaks.
Example: Mock a Repository Interface
type User = { id: string; email: string };
interface UserRepo {
findByEmail(email: string): Promise;
}
async function registerUser(repo: UserRepo, email: string) {
const existing = await repo.findByEmail(email);
if (existing) throw new Error('Email already used');
return { id: 'new', email };
}
Test With a Fake
import test from 'node:test';
import assert from 'node:assert/strict';
test('rejects duplicate email', async () => {
const repo = { findByEmail: async () => ({ id: '1', email: 'a@b.com' }) };
await assert.rejects(() => registerUser(repo, 'a@b.com'), /Email already used/);
});
Mocking Failure Modes
Mocks are especially valuable for simulating timeouts, retries, 429 rate limits, and partial outages. These are real production conditions that often go untested.
Over-Mocking Smells
- You mock 10 things just to test one route
- Tests assert internal call order instead of response behavior
- Refactors break tests even though behavior is unchanged