Master Jest's mocking system: create mock functions with jest.fn(), wrap real methods with jest.spyOn(), mock entire modules with jest.mock(), and verify call arguments precisely.
Isolation is the defining property of a unit test. When the function under test depends on a database, a payment gateway, or an email service, you cannot call the real thing — it is slow, non-deterministic, and has side effects. Mocking replaces those dependencies with controlled stand-ins that return predictable values and record how they were called.
Jest provides three mocking primitives: jest.fn() for creating standalone mock functions, jest.spyOn() for wrapping existing methods, and jest.mock() for replacing entire modules. Understanding when to use each — and how to clean up between tests — is the difference between a test suite that gives you confidence and one that lies to you.
jest.fn() creates a function that records every call made to it. You control what it returns and inspect how it was called.
1// Basic mock function
2const mockFn = jest.fn();
3mockFn('hello', 42);
4mockFn('world');
5
6console.log(mockFn.mock.calls); // [['hello', 42], ['world']]
7console.log(mockFn.mock.instances); // [undefined, undefined] (not called with new)
8console.log(mockFn.mock.results); // [{ type: 'return', value: undefined }, ...]
9
10// Controlling return values
11const getUser = jest.fn();
12getUser.mockReturnValue({ id: 1, name: 'Alice' }); // always returns this
13getUser.mockReturnValueOnce({ id: 2, name: 'Bob' }); // returns once, then default
14getUser.mockReturnValueOnce({ id: 3, name: 'Carol' }); // queue of one-time returns
15
16console.log(getUser()); // { id: 2, name: 'Bob' } (first call — one-time)
17console.log(getUser()); // { id: 3, name: 'Carol' } (second call — one-time)
18console.log(getUser()); // { id: 1, name: 'Alice' } (third call — default)
19
20// Async mock functions
21const fetchData = jest.fn();
22fetchData.mockResolvedValue({ status: 'ok', data: [] }); // Promise.resolve
23fetchData.mockRejectedValue(new Error('Network error')); // Promise.reject
24fetchData.mockResolvedValueOnce({ status: 'ok', data: [1] }); // once only
25
26// Custom implementation
27const multiply = jest.fn().mockImplementation((a: number, b: number) => a * b);
28console.log(multiply(3, 4)); // 12 — real logic, but still tracked
29
30// Verifying calls
31expect(multiply).toHaveBeenCalledTimes(1);
32expect(multiply).toHaveBeenCalledWith(3, 4);
33expect(multiply).toHaveReturnedWith(12);spyOn wraps an existing method on an object, letting you observe calls without replacing the real implementation (unless you explicitly override it).
1// src/logger.ts
2export const logger = {
3 info: (msg: string) => console.log(`[INFO] ${msg}`),
4 error: (msg: string) => console.error(`[ERROR] ${msg}`),
5};
6
7// src/order-service.ts
8import { logger } from './logger';
9
10export function processOrder(orderId: string): string {
11 logger.info(`Processing order ${orderId}`);
12 if (!orderId.startsWith('ORD-')) {
13 logger.error(`Invalid order ID: ${orderId}`);
14 throw new Error(`Invalid order ID: ${orderId}`);
15 }
16 return `confirmed-${orderId}`;
17}
18
19// src/order-service.test.ts
20import { logger } from './logger';
21import { processOrder } from './order-service';
22
23describe('processOrder', () => {
24 let infoSpy: jest.SpyInstance;
25 let errorSpy: jest.SpyInstance;
26
27 beforeEach(() => {
28 // Spy but keep the real implementation (useful for side-effect observation)
29 infoSpy = jest.spyOn(logger, 'info').mockImplementation(() => {}); // silence output
30 errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
31 });
32
33 afterEach(() => {
34 jest.restoreAllMocks(); // restores original implementations
35 });
36
37 it('logs the order id when processing starts', () => {
38 processOrder('ORD-001');
39 expect(infoSpy).toHaveBeenCalledWith('Processing order ORD-001');
40 });
41
42 it('logs an error when the order id is invalid', () => {
43 expect(() => processOrder('INVALID')).toThrow();
44 expect(errorSpy).toHaveBeenCalledWith('Invalid order ID: INVALID');
45 });
46
47 it('does not log an error for valid orders', () => {
48 processOrder('ORD-002');
49 expect(errorSpy).not.toHaveBeenCalled();
50 });
51});
52
53// Spying on class methods:
54class PaymentService {
55 charge(amount: number): Promise<string> {
56 // real implementation calls Stripe
57 return Promise.resolve('charge-id-123');
58 }
59}
60
61test('charge is called with the correct amount', async () => {
62 const service = new PaymentService();
63 const spy = jest.spyOn(service, 'charge').mockResolvedValue('mock-charge-id');
64
65 await service.charge(99.99);
66
67 expect(spy).toHaveBeenCalledWith(99.99);
68});jest.mock() replaces an entire module before any test in the file runs. Jest hoists the call to the top of the file at compile time, so it executes before imports.
1// src/email.ts
2export async function sendEmail(to: string, subject: string): Promise<void> {
3 // real SMTP call
4}
5
6// src/registration.ts
7import { sendEmail } from './email';
8
9export async function registerUser(email: string): Promise<{ id: string }> {
10 const user = { id: `user-${Date.now()}`, email };
11 await sendEmail(email, 'Welcome to DeveloperBootstrap!');
12 return user;
13}
14
15// src/registration.test.ts
16import { registerUser } from './registration';
17import { sendEmail } from './email';
18
19// Jest hoists this — runs before any import resolves
20jest.mock('./email', () => ({
21 sendEmail: jest.fn().mockResolvedValue(undefined),
22}));
23
24// Cast to access mock API
25const mockSendEmail = sendEmail as jest.Mock;
26
27describe('registerUser', () => {
28 beforeEach(() => {
29 mockSendEmail.mockClear();
30 });
31
32 it('returns a user object with an id', async () => {
33 const user = await registerUser('[email protected]');
34 expect(user.id).toBeDefined();
35 expect(user.email).toBe('[email protected]');
36 });
37
38 it('sends a welcome email to the registered address', async () => {
39 await registerUser('[email protected]');
40 expect(mockSendEmail).toHaveBeenCalledWith(
41 '[email protected]',
42 'Welcome to DeveloperBootstrap!'
43 );
44 });
45
46 it('calls sendEmail exactly once per registration', async () => {
47 await registerUser('[email protected]');
48 expect(mockSendEmail).toHaveBeenCalledTimes(1);
49 });
50
51 it('propagates email failures to the caller', async () => {
52 mockSendEmail.mockRejectedValueOnce(new Error('SMTP timeout'));
53 await expect(registerUser('[email protected]')).rejects.toThrow('SMTP timeout');
54 });
55});Jest provides a rich set of matchers for verifying how mocks were called:
1const spy = jest.fn();
2
3// Call the mock several times
4spy('first', 1);
5spy('second', 2);
6spy('third', 3);
7
8// Count assertions
9expect(spy).toHaveBeenCalledTimes(3);
10expect(spy).not.toHaveBeenCalledTimes(4);
11
12// Argument assertions
13expect(spy).toHaveBeenCalledWith('first', 1); // any call matches
14expect(spy).toHaveBeenLastCalledWith('third', 3); // most recent call
15expect(spy).toHaveBeenNthCalledWith(2, 'second', 2); // nth call (1-indexed)
16
17// Partial argument matching with expect helpers
18const apiSpy = jest.fn();
19apiSpy({ url: '/users', method: 'POST', body: { name: 'Alice' }, timeout: 5000 });
20
21expect(apiSpy).toHaveBeenCalledWith(
22 expect.objectContaining({
23 url: '/users',
24 method: 'POST',
25 // timeout is not asserted — we don't care about it
26 })
27);
28
29// Checking mock return values
30const calculator = jest.fn().mockImplementation((x: number) => x * 2);
31calculator(5);
32expect(calculator).toHaveReturnedWith(10);
33expect(calculator).toHaveReturnedTimes(1);
34
35// Accessing raw call data
36const emailMock = jest.fn();
37emailMock('[email protected]', 'Subject 1');
38emailMock('[email protected]', 'Subject 2');
39
40const [firstCall, secondCall] = emailMock.mock.calls;
41console.log(firstCall[0]); // '[email protected]'
42console.log(secondCall[1]); // 'Subject 2'Mock state (calls, instances, return value queues) accumulates across tests unless explicitly cleared. Three methods control this:
1// mockClear() — resets call history only; keeps implementation and return values
2// Use: when you want a fresh call count per test but keep mockImplementation
3jest.clearAllMocks();
4
5// mockReset() — resets call history + return values + implementation
6// Use: when you want a completely blank mock for the next test
7jest.resetAllMocks();
8
9// mockRestore() — restores spied-on methods to their original implementation
10// Only applies to jest.spyOn() mocks; no-op on jest.fn()
11jest.restoreAllMocks();
12
13// Configuring automatic reset in jest.config.ts (recommended for large projects)
14const config: Config = {
15 clearMocks: true, // calls mockClear after each test
16 resetMocks: false, // calls mockReset after each test
17 restoreMocks: true, // calls mockRestore after each test
18};
19
20// Practical example showing why order matters
21describe('mock lifecycle', () => {
22 const mockFetch = jest.fn();
23
24 beforeEach(() => {
25 // clearAllMocks keeps mockReturnValue set in beforeAll
26 // resetAllMocks removes mockReturnValue — choose based on your needs
27 jest.clearAllMocks();
28 });
29
30 it('test A sets up its own mock return', () => {
31 mockFetch.mockReturnValue({ data: 'test-a' });
32 expect(mockFetch()).toEqual({ data: 'test-a' });
33 expect(mockFetch).toHaveBeenCalledTimes(1);
34 });
35
36 it('test B starts with zero call count due to clearAllMocks', () => {
37 // call count is 0 because clearAllMocks ran between tests
38 expect(mockFetch).toHaveBeenCalledTimes(0);
39 // mockReturnValue from test A is still set (clearAllMocks does NOT remove it)
40 });
41});Recommended configuration: set restoreMocks: true and clearMocks: true in jest.config.ts to automatically clean up spies and reset call counts after each test.
A payment processing service tested with jest.fn(), jest.spyOn(), and jest.mock() to completely isolate the unit from Stripe, the database, and the logger.
1// src/stripe-client.ts
2export const stripeClient = {
3 charges: {
4 create: async (params: { amount: number; currency: string; source: string }) =>
5 ({ id: 'ch_real', amount: params.amount, status: 'succeeded' }),
6 },
7};
8
9// src/payment-service.ts
10import { stripeClient } from './stripe-client';
11import { logger } from './logger';
12
13export interface PaymentResult {
14 chargeId: string;
15 amount: number;
16 status: 'succeeded' | 'failed';
17}
18
19export async function processPayment(
20 amount: number,
21 currency: string,
22 cardToken: string
23): Promise<PaymentResult> {
24 if (amount <= 0) {
25 throw new Error('Payment amount must be greater than 0');
26 }
27
28 logger.info(`Processing payment: ${amount} ${currency}`);
29
30 try {
31 const charge = await stripeClient.charges.create({
32 amount: Math.round(amount * 100), // convert to cents
33 currency,
34 source: cardToken,
35 });
36
37 logger.info(`Payment succeeded: ${charge.id}`);
38 return { chargeId: charge.id, amount, status: 'succeeded' };
39 } catch (error) {
40 logger.error(`Payment failed: ${(error as Error).message}`);
41 return { chargeId: '', amount, status: 'failed' };
42 }
43}
44
45// src/payment-service.test.ts
46import { processPayment } from './payment-service';
47import { stripeClient } from './stripe-client';
48import { logger } from './logger';
49
50jest.mock('./stripe-client', () => ({
51 stripeClient: {
52 charges: {
53 create: jest.fn(),
54 },
55 },
56}));
57
58const mockCreate = stripeClient.charges.create as jest.Mock;
59
60describe('processPayment', () => {
61 let infoSpy: jest.SpyInstance;
62 let errorSpy: jest.SpyInstance;
63
64 beforeEach(() => {
65 jest.clearAllMocks();
66 infoSpy = jest.spyOn(logger, 'info').mockImplementation(() => {});
67 errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
68 mockCreate.mockResolvedValue({ id: 'ch_test_001', amount: 9999, status: 'succeeded' });
69 });
70
71 afterEach(() => {
72 jest.restoreAllMocks();
73 });
74
75 it('throws when amount is zero or negative', async () => {
76 await expect(processPayment(0, 'usd', 'tok_test')).rejects.toThrow(
77 'Payment amount must be greater than 0'
78 );
79 await expect(processPayment(-10, 'usd', 'tok_test')).rejects.toThrow(
80 'Payment amount must be greater than 0'
81 );
82 expect(mockCreate).not.toHaveBeenCalled();
83 });
84
85 it('calls Stripe with amount converted to cents', async () => {
86 await processPayment(99.99, 'usd', 'tok_visa');
87
88 expect(mockCreate).toHaveBeenCalledWith({
89 amount: 9999,
90 currency: 'usd',
91 source: 'tok_visa',
92 });
93 });
94
95 it('returns succeeded result with the Stripe charge id', async () => {
96 const result = await processPayment(50, 'usd', 'tok_visa');
97
98 expect(result).toEqual({
99 chargeId: 'ch_test_001',
100 amount: 50,
101 status: 'succeeded',
102 });
103 });
104
105 it('logs info messages on success', async () => {
106 await processPayment(50, 'usd', 'tok_visa');
107
108 expect(infoSpy).toHaveBeenCalledTimes(2);
109 expect(infoSpy).toHaveBeenNthCalledWith(1, 'Processing payment: 50 usd');
110 expect(infoSpy).toHaveBeenNthCalledWith(2, 'Payment succeeded: ch_test_001');
111 });
112
113 it('returns failed status and logs error when Stripe throws', async () => {
114 mockCreate.mockRejectedValueOnce(new Error('Card declined'));
115
116 const result = await processPayment(50, 'usd', 'tok_declined');
117
118 expect(result.status).toBe('failed');
119 expect(result.chargeId).toBe('');
120 expect(errorSpy).toHaveBeenCalledWith('Payment failed: Card declined');
121 });
122});This example combines all three mocking primitives: jest.mock() to wholesale replace the Stripe module (so no real HTTP calls), jest.spyOn() to silently capture logger calls, and mockCreate as a jest.Mock to control Stripe responses per test. Notice jest.clearAllMocks() in beforeEach resets call counts and mockResolvedValue queues, while jest.restoreAllMocks() in afterEach puts the real logger methods back. Each test verifies one facet of processPayment behaviour: validation, Stripe argument conversion, result shape, logging, and error handling.
The next lesson goes deeper on module mocks: manual mocks in __mocks__ directories, auto-mocking with jest.createMockFromModule(), and mocking Node built-ins like fs and path.
Your progress is saved in this browser, on this device only.