Mock Functions
Overview
Mock functions are essential for testing in isolation, controlling dependencies, and verifying how your code interacts with external systems. Fauji provides a comprehensive mocking system that includes spies, stubs, mocks, and module mocking capabilities.
Why Use Mock Functions?
- Isolation - Test units in isolation from their dependencies
- Control - Control the behavior of external functions and modules
- Verification - Verify that functions are called with expected arguments
- Performance - Avoid expensive operations like network calls or database queries
- Reliability - Create predictable test environments
Mock Function Types
- Spies - Track function calls without changing behavior
- Stubs - Replace method implementations while tracking calls
- Mocks - Complete function replacements with configurable behavior
- Module Mocks - Replace entire modules or specific exports
Creating Mock Functions
fn()
- Mock Functions
Creates a new mock function that tracks calls and allows behavior configuration.
describe('Mock Functions', () => {
test('creates basic mock function', () => {
const mockFn = fn();
// Call the mock function
mockFn('hello', 'world');
mockFn(42);
// Verify calls
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello', 'world');
expect(mockFn).toHaveBeenLastCalledWith(42);
});
test('mock function with implementation', () => {
const mockFn = fn((x, y) => x + y);
const result = mockFn(3, 4);
expect(result).toBe(7);
expect(mockFn).toHaveBeenCalledWith(3, 4);
});
test('mock function with return value', () => {
const mockFn = fn().mockReturnValue('mocked result');
const result = mockFn();
expect(result).toBe('mocked result');
expect(mockFn).toHaveBeenCalled();
});
});
spy(originalFunction)
- Function Spies
Creates a spy that wraps an existing function, allowing you to track calls while preserving original behavior.
describe('Spies', () => {
test('spy on existing function', () => {
const originalFunction = (a, b) => a * b;
const spiedFunction = spy(originalFunction);
const result = spiedFunction(3, 4);
expect(result).toBe(12); // Original behavior preserved
expect(spiedFunction).toHaveBeenCalledWith(3, 4);
expect(spiedFunction.callCount).toBe(1);
});
test('spy tracks multiple calls', () => {
const calculator = (operation, a, b) => {
switch(operation) {
case 'add': return a + b;
case 'multiply': return a * b;
default: return 0;
}
};
const spiedCalculator = spy(calculator);
spiedCalculator('add', 2, 3);
spiedCalculator('multiply', 4, 5);
expect(spiedCalculator).toHaveBeenCalledTimes(2);
expect(spiedCalculator).toHaveBeenNthCalledWith(1, 'add', 2, 3);
expect(spiedCalculator).toHaveBeenNthCalledWith(2, 'multiply', 4, 5);
});
});
spyOn(object, methodName)
- Method Spies
Creates a spy on an object's method, allowing you to track calls and optionally replace the implementation.
describe('Method Spies', () => {
test('spy on object method', () => {
const user = {
name: 'John',
greet: function(message) {
return `${message}, ${this.name}!`;
}
};
const greetSpy = spyOn(user, 'greet');
const result = user.greet('Hello');
expect(result).toBe('Hello, John!');
expect(greetSpy).toHaveBeenCalledWith('Hello');
});
test('spy with custom implementation', () => {
const apiService = {
fetchData: () => 'real data'
};
const fetchSpy = spyOn(apiService, 'fetchData')
.mockImplementation(() => 'mocked data');
const result = apiService.fetchData();
expect(result).toBe('mocked data');
expect(fetchSpy).toHaveBeenCalled();
});
test('restore original method', () => {
const mathUtils = {
multiply: (a, b) => a * b
};
const multiplySpy = spyOn(mathUtils, 'multiply')
.mockReturnValue(999);
expect(mathUtils.multiply(3, 4)).toBe(999);
multiplySpy.restore();
expect(mathUtils.multiply(3, 4)).toBe(12); // Original behavior restored
});
});
Mock Function Configuration
Return Values
describe('Mock Return Values', () => {
test('mock return value', () => {
const mockFn = fn().mockReturnValue('static value');
expect(mockFn()).toBe('static value');
expect(mockFn(1, 2, 3)).toBe('static value'); // Always returns same value
});
test('mock different return values', () => {
const mockFn = fn()
.mockReturnValueOnce('first call')
.mockReturnValueOnce('second call')
.mockReturnValue('default value');
expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');
expect(mockFn()).toBe('default value');
expect(mockFn()).toBe('default value'); // Continues returning default
});
test('mock resolved promises', () => {
const mockAsyncFn = fn().mockResolvedValue('async result');
return expect(mockAsyncFn()).resolves.toBe('async result');
});
test('mock rejected promises', () => {
const mockAsyncFn = fn().mockRejectedValue(new Error('Async error'));
return expect(mockAsyncFn()).rejects.toThrow('Async error');
});
});
Custom Implementations
describe('Mock Implementations', () => {
test('custom implementation', () => {
const mockFn = fn().mockImplementation((name) => `Hello, ${name}!`);
expect(mockFn('Alice')).toBe('Hello, Alice!');
expect(mockFn('Bob')).toBe('Hello, Bob!');
});
test('implementation once', () => {
const mockFn = fn()
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call')
.mockImplementation(() => 'default implementation');
expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');
expect(mockFn()).toBe('default implementation');
});
test('async implementation', () => {
const mockAsyncFn = fn().mockImplementation(async (delay) => {
await new Promise(resolve => setTimeout(resolve, delay));
return `Waited ${delay}ms`;
});
return expect(mockAsyncFn(100)).resolves.toBe('Waited 100ms');
});
test('throwing implementation', () => {
const mockFn = fn().mockImplementation(() => {
throw new Error('Mock error');
});
expect(() => mockFn()).toThrow('Mock error');
expect(mockFn).toHaveBeenCalled();
});
});
Mock Function Matchers
Call Verification
describe('Call Verification', () => {
test('verify function was called', () => {
const mockFn = fn();
expect(mockFn).not.toHaveBeenCalled();
mockFn();
expect(mockFn).toHaveBeenCalled();
});
test('verify call count', () => {
const mockFn = fn();
mockFn();
mockFn();
mockFn();
expect(mockFn).toHaveBeenCalledTimes(3);
});
test('verify call arguments', () => {
const mockFn = fn();
mockFn('hello', 'world', 42);
expect(mockFn).toHaveBeenCalledWith('hello', 'world', 42);
});
test('verify specific call', () => {
const mockFn = fn();
mockFn('first');
mockFn('second');
mockFn('third');
expect(mockFn).toHaveBeenNthCalledWith(1, 'first');
expect(mockFn).toHaveBeenNthCalledWith(2, 'second');
expect(mockFn).toHaveBeenLastCalledWith('third');
});
test('verify exact arguments', () => {
const mockFn = fn();
mockFn('hello', 'world');
// Regular toHaveBeenCalledWith uses deep equality
expect(mockFn).toHaveBeenCalledWith('hello', 'world');
// toHaveBeenCalledWithExactly uses strict equality
expect(mockFn).toHaveBeenCalledWithExactly('hello', 'world');
});
});
Return Value Verification
describe('Return Value Verification', () => {
test('verify function returned', () => {
const mockFn = fn(() => 'result');
mockFn();
expect(mockFn).toHaveReturned();
});
test('verify return value', () => {
const mockFn = fn(() => 'specific result');
mockFn();
expect(mockFn).toHaveReturnedWith('specific result');
});
test('verify last return value', () => {
const mockFn = fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValue('last');
mockFn();
mockFn();
mockFn();
expect(mockFn).toHaveLastReturnedWith('last');
});
test('verify nth return value', () => {
const mockFn = fn()
.mockReturnValueOnce('first')
.mockReturnValueOnce('second')
.mockReturnValueOnce('third');
mockFn();
mockFn();
mockFn();
expect(mockFn).toHaveNthReturnedWith(1, 'first');
expect(mockFn).toHaveNthReturnedWith(2, 'second');
expect(mockFn).toHaveNthReturnedWith(3, 'third');
});
});
Error Verification
describe('Error Verification', () => {
test('verify function threw', () => {
const mockFn = fn(() => {
throw new Error('Test error');
});
expect(() => mockFn()).toThrow();
expect(mockFn).toHaveThrown();
});
test('verify specific error', () => {
const mockFn = fn(() => {
throw new TypeError('Invalid type');
});
expect(() => mockFn()).toThrow();
expect(mockFn).toHaveThrownWith(TypeError);
expect(mockFn).toHaveThrownWith('Invalid type');
expect(mockFn).toHaveThrownWith(/Invalid/);
});
});
Module Mocking
Mocking External Modules
describe('Module Mocking', () => {
test('mock entire module', () => {
// Mock the fs module
const mockFs = mock('fs', {
readFileSync: fn().mockReturnValue('mocked file content'),
writeFileSync: fn(),
existsSync: fn().mockReturnValue(true)
});
const fs = require('fs');
expect(fs.readFileSync('test.txt')).toBe('mocked file content');
expect(fs.existsSync('test.txt')).toBe(true);
fs.writeFileSync('output.txt', 'data');
expect(fs.writeFileSync).toHaveBeenCalledWith('output.txt', 'data');
// Restore original module
mockFs.restore();
});
test('mock specific module functions', () => {
const mockHttp = mock('http', {
createServer: fn().mockReturnValue({
listen: fn(),
close: fn()
}),
get: fn().mockImplementation((url, callback) => {
callback({ statusCode: 200, data: 'mocked response' });
})
});
const http = require('http');
const server = http.createServer();
expect(http.createServer).toHaveBeenCalled();
server.listen(3000);
expect(server.listen).toHaveBeenCalledWith(3000);
mockHttp.restore();
});
});
Partial Module Mocking
describe('Partial Module Mocking', () => {
test('mock only specific exports', () => {
// Get actual module first
const actualUtils = requireActual('./utils');
// Mock specific functions while keeping others
const mockUtils = mock('./utils', {
...actualUtils,
fetchData: fn().mockResolvedValue({ id: 1, name: 'Mocked User' }),
// Keep other functions from actual module
});
const utils = require('./utils');
// Mocked function
return expect(utils.fetchData()).resolves.toMatchObject({
id: 1,
name: 'Mocked User'
});
// Other functions work normally
// expect(utils.formatDate(new Date())).toBe(actualUtils.formatDate(new Date()));
});
});
Real-World Testing Examples
API Client Testing
describe('API Client', () => {
let apiClient;
let mockFetch;
beforeEach(() => {
mockFetch = fn();
global.fetch = mockFetch;
apiClient = new APIClient('https://api.example.com');
});
afterEach(() => {
resetAllMocks();
});
test('makes GET request with correct parameters', async () => {
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ id: 1, name: 'John' })
});
const result = await apiClient.get('/users/1');
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/1', {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
expect(result).toMatchObject({ id: 1, name: 'John' });
});
test('handles POST requests with data', async () => {
const userData = { name: 'Jane', email: 'jane@example.com' };
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ id: 2, ...userData })
});
const result = await apiClient.post('/users', userData);
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
});
expect(result).toMatchObject({ id: 2, name: 'Jane' });
});
test('handles network errors', async () => {
mockFetch.mockRejectedValue(new Error('Network error'));
await expect(apiClient.get('/users')).rejects.toThrow('Network error');
expect(mockFetch).toHaveBeenCalled();
});
});
Event Handler Testing
describe('Event Handler', () => {
test('calls event listeners in correct order', () => {
const eventEmitter = new EventEmitter();
const listener1 = fn();
const listener2 = fn();
const listener3 = fn();
eventEmitter.on('test', listener1);
eventEmitter.on('test', listener2);
eventEmitter.on('test', listener3);
eventEmitter.emit('test', 'data');
expect(listener1).toHaveBeenCalledWith('data');
expect(listener2).toHaveBeenCalledWith('data');
expect(listener3).toHaveBeenCalledWith('data');
// Verify call order
expect(listener1).toHaveBeenCalledBefore(listener2);
expect(listener2).toHaveBeenCalledBefore(listener3);
});
test('removes event listeners correctly', () => {
const eventEmitter = new EventEmitter();
const listener = fn();
eventEmitter.on('test', listener);
eventEmitter.emit('test');
expect(listener).toHaveBeenCalledTimes(1);
eventEmitter.off('test', listener);
eventEmitter.emit('test');
expect(listener).toHaveBeenCalledTimes(1); // Not called again
});
});
Service Integration Testing
describe('User Service', () => {
let userService;
let mockDatabase;
let mockEmailService;
beforeEach(() => {
mockDatabase = {
findById: fn(),
save: fn(),
delete: fn()
};
mockEmailService = {
sendWelcomeEmail: fn(),
sendPasswordResetEmail: fn()
};
userService = new UserService(mockDatabase, mockEmailService);
});
test('creates user and sends welcome email', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const savedUser = { id: 1, ...userData };
mockDatabase.save.mockResolvedValue(savedUser);
mockEmailService.sendWelcomeEmail.mockResolvedValue(true);
const result = await userService.createUser(userData);
expect(mockDatabase.save).toHaveBeenCalledWith(userData);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(savedUser);
expect(result).toMatchObject(savedUser);
});
test('handles database errors gracefully', async () => {
const userData = { name: 'John', email: 'john@example.com' };
mockDatabase.save.mockRejectedValue(new Error('Database error'));
await expect(userService.createUser(userData)).rejects.toThrow('Database error');
expect(mockDatabase.save).toHaveBeenCalledWith(userData);
expect(mockEmailService.sendWelcomeEmail).not.toHaveBeenCalled();
});
test('retries failed email sending', async () => {
const userData = { name: 'John', email: 'john@example.com' };
const savedUser = { id: 1, ...userData };
mockDatabase.save.mockResolvedValue(savedUser);
mockEmailService.sendWelcomeEmail
.mockRejectedValueOnce(new Error('Email service down'))
.mockRejectedValueOnce(new Error('Email service down'))
.mockResolvedValue(true);
const result = await userService.createUser(userData);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledTimes(3);
expect(result).toMatchObject(savedUser);
});
});
Mock Management
Clearing and Resetting Mocks
describe('Mock Management', () => {
test('clear mock calls', () => {
const mockFn = fn();
mockFn('first call');
mockFn('second call');
expect(mockFn).toHaveBeenCalledTimes(2);
mockFn.mockClear();
expect(mockFn).toHaveBeenCalledTimes(0);
expect(mockFn.calls).toHaveLength(0);
});
test('reset mock implementation', () => {
const mockFn = fn()
.mockReturnValue('mocked')
.mockImplementation(() => 'implemented');
expect(mockFn()).toBe('implemented');
mockFn.mockReset();
expect(mockFn()).toBeUndefined(); // Back to default behavior
expect(mockFn).toHaveBeenCalledTimes(1); // Calls are cleared too
});
test('restore mock to original', () => {
const original = () => 'original';
const mockFn = fn(original).mockReturnValue('mocked');
expect(mockFn()).toBe('mocked');
mockFn.mockRestore();
expect(mockFn()).toBe('original'); // Original behavior restored
});
});
Global Mock Management
describe('Global Mock Management', () => {
afterEach(() => {
// Clear all mocks after each test
resetAllMocks();
});
test('reset all mocks globally', () => {
const mock1 = fn();
const mock2 = fn();
const mock3 = fn();
mock1('test');
mock2('test');
mock3('test');
expect(mock1).toHaveBeenCalled();
expect(mock2).toHaveBeenCalled();
expect(mock3).toHaveBeenCalled();
resetAllMocks();
expect(mock1).not.toHaveBeenCalled();
expect(mock2).not.toHaveBeenCalled();
expect(mock3).not.toHaveBeenCalled();
});
});
Advanced Mock Patterns
Conditional Mocking
describe('Conditional Mocking', () => {
test('mock based on input', () => {
const mockApi = fn().mockImplementation((endpoint) => {
switch (endpoint) {
case '/users':
return Promise.resolve([{ id: 1, name: 'John' }]);
case '/posts':
return Promise.resolve([{ id: 1, title: 'Test Post' }]);
default:
return Promise.reject(new Error('Not found'));
}
});
return Promise.all([
expect(mockApi('/users')).resolves.toHaveLength(1),
expect(mockApi('/posts')).resolves.toMatchObject([{ title: 'Test Post' }]),
expect(mockApi('/invalid')).rejects.toThrow('Not found')
]);
});
test('stateful mock', () => {
let callCount = 0;
const statefulMock = fn().mockImplementation(() => {
callCount++;
return `Call number ${callCount}`;
});
expect(statefulMock()).toBe('Call number 1');
expect(statefulMock()).toBe('Call number 2');
expect(statefulMock()).toBe('Call number 3');
});
});
Mock Chains and Fluent APIs
describe('Mock Chains', () => {
test('mock fluent API', () => {
const mockBuilder = {
select: fn().mockReturnThis(),
where: fn().mockReturnThis(),
orderBy: fn().mockReturnThis(),
limit: fn().mockReturnThis(),
execute: fn().mockResolvedValue([{ id: 1, name: 'Result' }])
};
const queryBuilder = mockBuilder;
return queryBuilder
.select('name')
.where('active', true)
.orderBy('created_at')
.limit(10)
.execute()
.then(results => {
expect(mockBuilder.select).toHaveBeenCalledWith('name');
expect(mockBuilder.where).toHaveBeenCalledWith('active', true);
expect(mockBuilder.orderBy).toHaveBeenCalledWith('created_at');
expect(mockBuilder.limit).toHaveBeenCalledWith(10);
expect(mockBuilder.execute).toHaveBeenCalled();
expect(results).toHaveLength(1);
});
});
});
Best Practices
When to Use Each Mock Type
- Spies: When you want to track calls but keep original behavior
- Stubs: When you need to replace a method temporarily
- Mocks: When you need complete control over function behavior
- Module Mocks: When isolating external dependencies
Mock Function Guidelines
describe('Mock Best Practices', () => {
// ✅ Good: Descriptive mock implementations
test('good mock implementation', () => {
const mockUserService = {
findById: fn().mockImplementation((id) => {
if (id === 1) return Promise.resolve({ id: 1, name: 'John' });
return Promise.reject(new Error('User not found'));
})
};
// Clear expectation of what the mock does
});
// ❌ Bad: Unclear mock behavior
test('bad mock implementation', () => {
const mockUserService = {
findById: fn().mockReturnValue('anything')
};
// Unclear what this represents
});
// ✅ Good: Clean up mocks
afterEach(() => {
resetAllMocks();
});
// ✅ Good: Verify interactions
test('verify important interactions', () => {
const mockLogger = fn();
processData('test', mockLogger);
expect(mockLogger).toHaveBeenCalledWith('Processing: test');
});
});
run();
Common Pitfalls
- Over-mocking: Don't mock everything; test real integrations when possible
- Brittle tests: Avoid testing implementation details
- Forgetting cleanup: Always reset mocks between tests
- Complex mocks: Keep mock implementations simple and focused
- Not verifying calls: Always verify that mocks were called as expected
Integration with Other Features
Mock functions integrate seamlessly with other Fauji features:
- Async Testing: Mock async functions and verify promise behavior
- Setup/Teardown: Initialize and clean up mocks in lifecycle hooks
- Custom Matchers: Create custom matchers for mock verification
- Error Testing: Mock functions that throw errors for error path testing
For more testing patterns, see:
- Async Testing Guide for async mock patterns
- Setup & Teardown Guide for mock lifecycle management