Docs
README
20.3 Mocking & Test Doubles
Overview
Mocking and test doubles are techniques for replacing real dependencies with controlled substitutes during testing. They allow you to isolate the code under test and verify behavior without relying on external systems.
Learning Objectives
By the end of this section, you'll understand:
- β’Types of test doubles (Dummy, Stub, Spy, Mock, Fake)
- β’When to use each type
- β’Implementing test doubles manually
- β’Using mocking libraries
- β’Best practices for mocking
Types of Test Doubles
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Test Double Spectrum β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Simple βββββββββββββββββββββββββββββββββββββββββββΊ Complex β
β β
β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β
β β Dummy β β Stub β β Spy β β Mock β β
β β β β β β β β β β
β β Passed β β Returns β β Records β β Verifiesβ β
β β but not β β canned β β calls β β expecta-β β
β β used β β answers β β & args β β tions β β
β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β
β β β β β β
β βββββββββββββββ΄ββββββββββββββ΄ββββββββββββββ β
β β β
β ββββββββ΄βββββββ β
β β Fake β β
β β β β
β β Working but β β
β β simplified β β
β β implementation β
β βββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Test Double Comparison
| Type | Purpose | Behavior | Example |
|---|---|---|---|
| Dummy | Fill parameters | Not actually used | null, empty object |
| Stub | Provide canned answers | Returns predefined values | getUser() => user |
| Spy | Track calls | Records calls + can return values | sinon.spy() |
| Mock | Verify behavior | Has expectations, can fail | expect(fn).toHaveBeenCalled() |
| Fake | Working substitute | Simpler implementation | In-memory database |
When to Use Test Doubles
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β When to Use Test Doubles β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β USE Mocks When: DON'T Mock When: β
β ββββββββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β β β β β
β β β Dependency is slow β β β Simple value objects β β
β β (database, network) β β β β
β β β β β Pure utility funcs β β
β β β Dependency is costly β β β β
β β (payment APIs) β β β Everything - only β β
β β β β what's needed β β
β β β Dependency is β β β β
β β non-deterministic β β β Implementation β β
β β (time, random) β β details β β
β β β β β β
β β β Dependency has β β β β
β β side effects β β β β
β β (sending emails) β β β β
β β β β β β
β ββββββββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Test Double Examples
Dummy
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Dummy β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β function createUser(name, logger) { β
β return { name }; β
β } β
β β
β // Test - logger not used but required β
β const dummyLogger = null; βββ Dummy β
β const user = createUser('John', dummyLogger); β
β β
β Purpose: Satisfy parameter requirements β
β Behavior: Never actually called β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Stub
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Stub β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β class UserService { β
β constructor(userRepository) { β
β this.repo = userRepository; β
β } β
β async getUser(id) { β
β return this.repo.findById(id); β
β } β
β } β
β β
β // Stub - returns canned data β
β const stubRepo = { βββ Stub β
β findById: (id) => ({ β
β id, name: 'John' β
β }) β
β }; β
β β
β const service = new UserService(stubRepo); β
β const user = await service.getUser(1); β
β // user = { id: 1, name: 'John' } β
β β
β Purpose: Control indirect inputs β
β Behavior: Returns predefined values β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Spy
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Spy β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β class NotificationService { β
β constructor(emailSender) { β
β this.sender = emailSender; β
β } β
β notify(user, message) { β
β this.sender.send(user.email, message); β
β } β
β } β
β β
β // Spy - records calls β
β const spySender = { βββ Spy β
β calls: [], β
β send(to, msg) { β
β this.calls.push({ to, msg }); β
β } β
β }; β
β β
β const service = new NotificationService(spySender); β
β service.notify({ email: 'a@b.com' }, 'Hello'); β
β β
β // Assert on recorded calls β
β assert(spySender.calls[0].to === 'a@b.com'); β
β assert(spySender.calls[0].msg === 'Hello'); β
β β
β Purpose: Verify indirect outputs β
β Behavior: Records all calls for later inspection β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Mock
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mock β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β // Mock with expectations β
β const mockPayment = { βββ Mock β
β expectations: [], β
β calls: [], β
β β
β expect(method, args, returns) { β
β this.expectations.push({ method, args, returns }); β
β }, β
β β
β charge(amount) { β
β this.calls.push({ method: 'charge', args: [amount] });
β const exp = this.expectations.find( β
β e => e.method === 'charge' β
β ); β
β return exp?.returns; β
β }, β
β β
β verify() { β
β this.expectations.forEach(exp => { β
β const call = this.calls.find( β
β c => c.method === exp.method β
β ); β
β if (!call) throw new Error('Expectation not met');
β }); β
β } β
β }; β
β β
β mockPayment.expect('charge', [100], { success: true }); β
β processOrder(mockPayment, order); β
β mockPayment.verify(); // Throws if charge wasn't called β
β β
β Purpose: Verify behavior + provide returns β
β Behavior: Has expectations that must be met β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Fake
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Fake β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β // Fake - working but simplified implementation β
β class FakeDatabase { βββ Fake β
β constructor() { β
β this.data = new Map(); β
β } β
β β
β insert(table, record) { β
β const id = this.data.size + 1; β
β this.data.set(`${table}:${id}`, { ...record, id }); β
β return id; β
β } β
β β
β find(table, id) { β
β return this.data.get(`${table}:${id}`); β
β } β
β β
β update(table, id, data) { β
β const key = `${table}:${id}`; β
β const existing = this.data.get(key); β
β this.data.set(key, { ...existing, ...data }); β
β } β
β β
β delete(table, id) { β
β this.data.delete(`${table}:${id}`); β
β } β
β } β
β β
β Purpose: Simplified working implementation β
β Behavior: Works like real thing but simpler/faster β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Mocking Strategies
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mocking Strategies β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Dependency Injection β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β class Service { β β
β β constructor(dependency) { β Injected β β
β β this.dep = dependency; β β
β β } β β
β β } β β
β β // Test: new Service(mockDependency) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 2. Module Mocking β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // Mock the entire module β β
β β jest.mock('./database', () => ({ β β
β β query: jest.fn().mockResolvedValue([...]) β β
β β })); β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β 3. Monkey Patching (Last Resort!) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β const original = obj.method; β β
β β obj.method = mockFn; β β
β β // ... test ... β β
β β obj.method = original; // Restore! β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Mock Patterns
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Common Mock Patterns β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Time Mock β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β const realDate = Date; β β
β β global.Date = class extends Date { β β
β β constructor() { β β
β β super(); β β
β β return new realDate('2024-01-01'); β β
β β } β β
β β }; β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Random Mock β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β const originalRandom = Math.random; β β
β β Math.random = () => 0.5; // Deterministic β β
β β // ... test ... β β
β β Math.random = originalRandom; β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Async Mock β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β const mockFetch = (url) => Promise.resolve({ β β
β β ok: true, β β
β β json: () => Promise.resolve({ data: 'test' }) β β
β β }); β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β Error Mock β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β const failingMock = () => { β β
β β throw new Error('Simulated failure'); β β
β β }; β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Mock Best Practices
| Practice | Description | Why |
|---|---|---|
| Mock at boundaries | Mock external systems, not internals | Reduces brittleness |
| Verify behavior | Check interactions, not implementation | Tests stay relevant |
| Keep mocks simple | Only mock what's needed | Easier maintenance |
| Use real objects when possible | Prefer integration over isolation | More realistic tests |
| Reset mocks between tests | Clear state for isolation | Prevents test pollution |
| Name mocks clearly | mockUserRepo, stubLogger | Better readability |
Anti-Patterns to Avoid
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Mocking Anti-Patterns β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β β Over-mocking β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // DON'T mock everything β β
β β mock(Array); β β
β β mock(Object); β β
β β mock(everyInternalClass); β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β Implementation-dependent mocks β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // DON'T verify private method calls β β
β β expect(service._privateMethod).toHaveBeenCalled(); β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β Brittle assertions β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // DON'T check exact call count if not important β β
β β expect(mock).toHaveBeenCalledTimes(3); β β
β β β β
β β // DO check that it was called β β
β β expect(mock).toHaveBeenCalled(); β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β β Mocking what you own β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β // DON'T mock your own classes if not needed β β
β β // Use the real implementation β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key Takeaways
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Test Doubles Summary β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β β Use dummies for unused but required parameters β
β β Use stubs to control indirect inputs β
β β Use spies to verify indirect outputs β
β β Use mocks for behavior verification β
β β Use fakes for complex working substitutes β
β β
β Key Principles: β
β β’ Mock external dependencies, not internal code β
β β’ Use dependency injection for testability β
β β’ Keep mocks simple and focused β
β β’ Verify behavior, not implementation β
β β’ Reset mocks between tests β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Navigation
- β’Previous: 20.2 Integration & E2E Testing
- β’Next: 20.4 Debugging Techniques