Don't replace mocks with slowdep
Jest mocks and slowdep serve different purposes at different levels of the test pyramid. Jest mocks give you isolation and control: every call returns exactly what you configured, instantly, with no side effects. That's exactly what you want for unit tests where you're testing a function's logic in isolation.
slowdep gives you realism: it wraps the real (or mock) function and adds a latency distribution. Use it when you're testing how your code behaves when a dependency is slow — not when you're testing what the dependency returns. These are separate concerns.
Unit tests: use Jest mocks for isolation, speed, and control. Integration tests: use slowdep (wrapping a mock or a real function) to add realistic latency.
Comparison table
| feature | jest.mock() | slowdep |
|---|---|---|
| response time | synchronous / instant | lognormal distribution (realistic) |
| error simulation | explicit — .mockRejectedValue() | probabilistic — errorRate option |
| realistic behavior | no — always instant | yes — lognormal latency |
| test isolation | complete — no side effects possible | partial — wraps real or mock function |
| setup complexity | zero — built into Jest | minimal — one import, one wrap |
| call tracking | full — .mock.calls, .mock.results | none — compose with jest.fn() for tracking |
| composable with real implementations | jest.requireActual() | wraps any function, real or mock |
| tests retry logic | no — errors are deterministic, not probabilistic | yes — probabilistic errors exercise retry paths |
The right tool for the right layer
jest.fn().mockResolvedValue({ id: 1 }) is the right tool.
withLatency(jest.fn().mockResolvedValue(data), 'postgres') gives you
the trackability of a Jest mock with realistic latency behavior.
Same scenario, different tools
// Unit test: testing UserService.getUser logic in isolation // Goal: verify the function parses DB response correctly // Latency doesn't matter here — we're testing logic jest.mock('../db'); import { db } from '../db'; import { UserService } from './UserService'; describe('UserService.getUser', () => { it('returns null when user not found', async () => { (db.query as jest.Mock).mockResolvedValue({ rows: [] }); const result = await new UserService(db).getUser('999'); expect(result).toBeNull(); }); it('maps rows to User objects', async () => { (db.query as jest.Mock).mockResolvedValue({ rows: [{ id: '1', name: 'Alice', email: 'alice@example.com' }] }); const user = await new UserService(db).getUser('1'); expect(user).toMatchObject({ name: 'Alice', email: 'alice@example.com' }); }); });
// Integration test: testing UserService under realistic DB latency // Goal: verify retry logic fires correctly under slow DB calls import { withLatency } from 'slowdep'; describe('UserService with realistic DB latency', () => { const realData = { rows: [{ id: '1', name: 'Alice' }] }; it('handles occasional DB slowness gracefully', async () => { const slowDb = { query: withLatency( async (sql: string) => realData, { p50: 5, p99: 200, errorRate: 0.05 } ), }; // run 20 calls — errorRate ensures retries get exercised const svc = new UserService(slowDb); const results = await Promise.allSettled( Array.from({ length: 20 }, () => svc.getUser('1')) ); const succeeded = results.filter(r => r.status === 'fulfilled'); expect(succeeded.length).toBeGreaterThan(15); // retries salvage most calls }); });
// Combined: jest.fn() for tracking + slowdep for latency import { withLatency } from 'slowdep'; describe('UserService — tracked realistic test', () => { it('calls DB exactly once per request under normal conditions', async () => { const baseQuery = jest.fn().mockResolvedValue({ rows: [{ id: '1', name: 'Alice' }] }); const slowDb = { // jest.fn() for call tracking, slowdep for latency query: withLatency(baseQuery, { p50: 5, p99: 200, errorRate: 0 }), }; const user = await new UserService(slowDb).getUser('1'); expect(user.name).toBe('Alice'); expect(baseQuery).toHaveBeenCalledTimes(1); // tracked by jest.fn() expect(baseQuery).toHaveBeenCalledWith( // verified call args expect.stringContaining('SELECT'), ['1'] ); }); });
The test pyramid
Think of the test pyramid: unit tests at the base (fast, isolated, Jest mocks), integration tests in the middle (realistic behavior, slowdep), E2E at the top (real services, Toxiproxy or staging). Each layer catches different bugs. Each needs different tools.
| layer | tool | what it catches |
|---|---|---|
| unit tests | jest.mock() | logic bugs, parsing bugs, branching |
| integration tests | slowdep + jest.fn() | retry bugs, timeout bugs, latency UX |
| E2E / staging | real services or Toxiproxy | network bugs, infra bugs, system-level behavior |