Mocking
Fake it till you make it - isolating code with mocks, stubs, and spies.
6 min read
Why Mock?#
Unit tests should be fast and isolated. But your code calls databases, APIs, sends emails. You don't want tests to:
- Actually send emails to users
- Hit production APIs
- Require a running database
- Take minutes to run
Mocking replaces real dependencies with fake ones you control.
Mocking Concepts#
Stub#
Returns predetermined data. Doesn't care how it's called.
javascript
// Stub: always returns this user
const userRepo = {
findById: () => ({ id: '123', name: 'John' }),
};
Spy#
Records how it was called. Can wrap real implementation.
javascript
// Spy: tracks calls
const spy = vi.spyOn(emailService, 'send');
await emailService.send('test@test.com');
expect(spy).toHaveBeenCalledWith('test@test.com');
Mock#
Programmable fake with assertions. Most versatile.
javascript
// Mock: fake with expectations
const mockRepo = {
findById: vi.fn().mockResolvedValue({ id: '123' }),
};
Mocking with Different Frameworks#
Vitest#
javascript
import { vi, describe, it, expect, beforeEach } from 'vitest';
describe('UserService', () => {
let mockDb;
beforeEach(() => {
mockDb = {
query: vi.fn(),
};
});
it('fetches user by id', async () => {
mockDb.query.mockResolvedValue([{ id: '1', name: 'John' }]);
const result = await mockDb.query('SELECT * FROM users WHERE id = ?', ['1']);
expect(result[0].name).toBe('John');
expect(mockDb.query).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = ?',
['1']
);
});
});
Jest#
javascript
// Same API as Vitest
const mockFn = jest.fn();
mockFn.mockReturnValue('value');
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('error'));
Node.js Test Runner#
javascript
import { mock } from 'node:test';
const mockFn = mock.fn(() => 'value');
mockFn();
// Check calls
console.log(mockFn.mock.calls.length); // 1
console.log(mockFn.mock.calls[0].arguments); // []
Common Mocking Patterns#
Mocking Return Values#
javascript
const mockFn = vi.fn();
// Return value
mockFn.mockReturnValue('sync value');
// Return promise
mockFn.mockResolvedValue('async value');
mockFn.mockRejectedValue(new Error('failed'));
// Return different values each call
mockFn
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('default');
console.log(mockFn()); // 'first'
console.log(mockFn()); // 'second'
console.log(mockFn()); // 'default'
console.log(mockFn()); // 'default'
Mocking Implementation#
javascript
const mockFn = vi.fn().mockImplementation((x) => x * 2);
console.log(mockFn(5)); // 10
// Different implementation each call
mockFn
.mockImplementationOnce((x) => x + 1)
.mockImplementationOnce((x) => x - 1);
console.log(mockFn(5)); // 6
console.log(mockFn(5)); // 4
Spying on Methods#
javascript
import { vi } from 'vitest';
const calculator = {
add: (a, b) => a + b,
};
// Spy on method (keeps original implementation)
const spy = vi.spyOn(calculator, 'add');
calculator.add(2, 3); // Returns 5 (real implementation)
expect(spy).toHaveBeenCalledWith(2, 3);
expect(spy).toHaveBeenCalledTimes(1);
// Can also replace implementation
spy.mockImplementation((a, b) => a * b);
calculator.add(2, 3); // Returns 6 (mocked)
Mocking Modules#
Vitest#
javascript
// Mock entire module
vi.mock('./database.js', () => ({
query: vi.fn().mockResolvedValue([]),
connect: vi.fn(),
}));
// Or auto-mock
vi.mock('./database.js');
// Then in test
import { query } from './database.js';
query.mockResolvedValue([{ id: 1 }]);
Jest#
javascript
// In test file or __mocks__ folder
jest.mock('./database.js', () => ({
query: jest.fn().mockResolvedValue([]),
}));
Manual Mocks#
javascript
// src/__mocks__/database.js
export const query = vi.fn();
export const connect = vi.fn();
// Automatically used when you call vi.mock('./database.js')
Mocking External Libraries#
Mocking fetch#
javascript
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('fetches data', async () => {
global.fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'test' }),
});
const response = await fetch('/api/data');
const data = await response.json();
expect(data).toEqual({ data: 'test' });
});
Mocking Axios#
javascript
import { vi } from 'vitest';
import axios from 'axios';
vi.mock('axios');
it('fetches users', async () => {
axios.get.mockResolvedValue({
data: [{ id: 1, name: 'John' }],
});
const response = await axios.get('/users');
expect(response.data[0].name).toBe('John');
});
Mocking File System#
javascript
import { vi } from 'vitest';
import fs from 'fs/promises';
vi.mock('fs/promises');
it('reads config file', async () => {
fs.readFile.mockResolvedValue('{"port": 3000}');
const config = JSON.parse(await fs.readFile('config.json', 'utf8'));
expect(config.port).toBe(3000);
});
Mocking Time#
javascript
import { vi, beforeEach, afterEach } from 'vitest';
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('expires token after 1 hour', () => {
const token = createToken();
// Fast forward 30 minutes
vi.advanceTimersByTime(30 * 60 * 1000);
expect(isTokenValid(token)).toBe(true);
// Fast forward another 31 minutes
vi.advanceTimersByTime(31 * 60 * 1000);
expect(isTokenValid(token)).toBe(false);
});
it('uses specific date', () => {
vi.setSystemTime(new Date('2024-01-01'));
expect(new Date().getFullYear()).toBe(2024);
});
Mocking Database#
With Dependency Injection#
javascript
// Production code
class UserService {
constructor(db) {
this.db = db;
}
async findById(id) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
// Test
const mockDb = {
query: vi.fn().mockResolvedValue([{ id: '1', name: 'John' }]),
};
const service = new UserService(mockDb);
const user = await service.findById('1');
expect(user.name).toBe('John');
Mocking Mongoose#
javascript
import { vi } from 'vitest';
import { User } from '../models/User.js';
vi.mock('../models/User.js', () => ({
User: {
findById: vi.fn(),
findOne: vi.fn(),
create: vi.fn(),
},
}));
it('finds user by id', async () => {
User.findById.mockResolvedValue({ id: '1', name: 'John' });
const user = await User.findById('1');
expect(user.name).toBe('John');
});
Assertion Matchers for Mocks#
javascript
const mockFn = vi.fn();
// Was called
expect(mockFn).toHaveBeenCalled();
expect(mockFn).not.toHaveBeenCalled();
// Call count
expect(mockFn).toHaveBeenCalledTimes(3);
// Called with specific args
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveBeenLastCalledWith('lastArg');
expect(mockFn).toHaveBeenNthCalledWith(1, 'firstArg');
// Partial matching
expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({ id: '123' })
);
expect(mockFn).toHaveBeenCalledWith(
expect.stringContaining('error')
);
expect(mockFn).toHaveBeenCalledWith(
expect.any(String)
);
Clearing and Resetting Mocks#
javascript
const mockFn = vi.fn().mockReturnValue('value');
mockFn('call 1');
mockFn('call 2');
// Clear calls but keep implementation
mockFn.mockClear();
expect(mockFn).not.toHaveBeenCalled();
console.log(mockFn()); // 'value' (implementation kept)
// Reset everything
mockFn.mockReset();
console.log(mockFn()); // undefined
// Restore original (for spies)
const spy = vi.spyOn(obj, 'method');
spy.mockRestore(); // Back to original
Best Practices#
Don't Over-Mock#
javascript
// BAD: Mocking everything
const result = add(mockA, mockB); // What are you testing?
// GOOD: Mock only external dependencies
const result = await userService.create(data); // Real logic, mocked DB
Reset Between Tests#
javascript
beforeEach(() => {
vi.clearAllMocks();
});
// Or
afterEach(() => {
vi.restoreAllMocks();
});
Use Factory Functions#
javascript
function createMockUser(overrides = {}) {
return {
id: '123',
email: 'test@test.com',
name: 'Test User',
...overrides,
};
}
it('test 1', () => {
const user = createMockUser({ name: 'John' });
});
it('test 2', () => {
const user = createMockUser({ email: 'john@test.com' });
});
Key Takeaways#
- Mock external dependencies - DB, APIs, file system
- Don't mock what you're testing - Test real logic
- Use dependency injection - Makes mocking easier
- Reset mocks between tests - Avoid test pollution
- Keep mocks simple - Complex mocks = fragile tests
The Danger
Over-mocking leads to tests that pass but code that breaks. If you're mocking everything, you're testing your mocks, not your code.
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.