JavaScript and TypeScript course Β· Module 11: Testing with Jest

Mocking in Jest

7 min read
In this lesson5

When the park's security system detects a breach, it sends an SMS to the rangers. A test of that system cannot really send messages every time it runs - it would be slow and network-dependent, and the rangers would soon stop reacting to alarms. We need a stand-in: something that looks like the real function but sends nothing, and instead records who called it and with what.

Mocking is creating artificial versions of functions, modules, or objects. In Jurassic Park, it is like creating dinosaur models for security tests instead of using real beasts.

jest.fn() - Creating Mock Functions

jest.fn() creates an "empty" function that records all calls. By default it returns undefined, and to check its history you use the matchers from the toHaveBeenCalled family:

1test('jest.fn() - basics', () => {
2  // Creating a mock function
3  const feedDino = jest.fn();
4
5  // Calling the mock function
6  feedDino('Rex', 'meat');
7  feedDino('Brachio', 'plants');
8
9  // Checking calls
10  expect(feedDino).toHaveBeenCalled();
11  expect(feedDino).toHaveBeenCalledTimes(2);
12  expect(feedDino).toHaveBeenCalledWith('Rex', 'meat');
13  expect(feedDino).toHaveBeenLastCalledWith('Brachio', 'plants');
14});

toHaveBeenCalled asks whether the function ran at all, toHaveBeenCalledTimes how many times, toHaveBeenCalledWith whether it received these arguments at least once, and toHaveBeenLastCalledWith what it was called with most recently. The mock fed nobody - it only noted that someone tried.

Mock with Implementation

You can give a mock a behavior: mockReturnValue sets the value returned on every call, mockReturnValueOnce on just one call, and a function passed to jest.fn becomes its full implementation:

1test('mock with implementation', () => {
2  // Mock with return value
3  const getHealth = jest.fn().mockReturnValue(100);
4  expect(getHealth()).toBe(100);
5
6  // Mock with different values for subsequent calls
7  const checkSensor = jest.fn()
8    .mockReturnValueOnce('OK')
9    .mockReturnValueOnce('WARNING')
10    .mockReturnValueOnce('DANGER');
11
12  expect(checkSensor()).toBe('OK');
13  expect(checkSensor()).toBe('WARNING');
14  expect(checkSensor()).toBe('DANGER');
15
16  // Mock with custom implementation
17  const calculateDamage = jest.fn((attack, defense) => {
18    return Math.max(0, attack - defense);
19  });
20
21  expect(calculateDamage(50, 30)).toBe(20);
22  expect(calculateDamage(10, 30)).toBe(0);
23});

The chain of mockReturnValueOnce calls replays a scenario step by step: the sensor reports OK, then a warning, and finally danger. Once the one-time values run out, the mock falls back to mockReturnValue, or to undefined if none was set. Asynchronous functions have counterparts: mockResolvedValue returns a promise fulfilled with the given value, and mockRejectedValue a rejected promise.

jest.mock() - Mocking Modules

jest.mock() allows you to replace an entire module with a mock. First, look at the module we do not want to run in tests - sendAlert sends an alert through fetch to the park's API:

1// File: alertService.js
2export function sendAlert(zone, message) {
3  // In reality sends email/SMS - we don't want this in tests!
4  return fetch('/api/alerts', {
5    method: 'POST',
6    body: JSON.stringify({ zone, message })
7  });
8}

In the test file, calling jest.mock('./alertService') replaces every function of that module with a mock that returns undefined, and require loads the already swapped version. SecuritySystem is the class under test, which uses alertService internally:

1// File: securitySystem.test.js
2jest.mock('./alertService');
3
4const { sendAlert } = require('./alertService');
5
6describe('SecuritySystem', () => {
7  beforeEach(() => {
8    // Reset mocks before each test
9    sendAlert.mockClear();
10    sendAlert.mockResolvedValue({ status: 'sent' });
11  });
12
13  it('should send alert on breach', async () => {
14    const system = new SecuritySystem();
15    await system.reportBreach('Zone A');
16
17    expect(sendAlert).toHaveBeenCalledWith(
18      'Zone A',
19      expect.stringContaining('breach')
20    );
21  });
22});

The test checks that the system asked for an alert to be sent, yet no SMS went out. expect.stringContaining is an asymmetric matcher: it accepts any string that contains the given fragment. Also note the order: with babel-jest, jest.mock calls are automatically moved to the top of the file (hoisting), so the mock works even when an import comes first.

jest.mock with a Factory

An automatic mock is not always enough. The second argument of jest.mock is a factory: a function that returns an object pretending to be the module. That gives you full control over what the module exports:

1jest.mock('./alertService', () => ({
2  sendAlert: jest.fn().mockResolvedValue({ status: 'sent' }),
3}));

This time sendAlert returns a fulfilled promise from the start, so the configuration in beforeEach is no longer needed. Hoisting has a consequence, though: the factory cannot use variables declared outside it, unless their names start with the word mock.

jest.spyOn() - Spying on Methods

spyOn monitors an existing method without replacing it (by default). The difference is fundamental: jest.fn creates a new, empty function, while jest.spyOn wraps a method that already exists on an object and still calls the original:

1test('spyOn - monitoring a method', () => {
2  const park = {
3    dinosaurs: [],
4    addDinosaur(dino) {
5      this.dinosaurs.push(dino);
6      return this.dinosaurs.length;
7    },
8    getDinosaurCount() {
9      return this.dinosaurs.length;
10    }
11  };
12
13  // Spy on the method - preserves original implementation
14  const spy = jest.spyOn(park, 'addDinosaur');
15
16  park.addDinosaur({ name: 'Rex' });
17  park.addDinosaur({ name: 'Blue' });
18
19  // Check calls
20  expect(spy).toHaveBeenCalledTimes(2);
21  expect(spy).toHaveBeenCalledWith({ name: 'Rex' });
22
23  // The original function still works!
24  expect(park.dinosaurs).toHaveLength(2);
25
26  // Restore original implementation
27  spy.mockRestore();
28});

The spy recorded both calls, and the dinosaurs still made it into the park, because the original method ran. mockRestore at the end removes the spy and restores the method from before the test. It works only for mocks created with spyOn.

spyOn with Implementation Replacement

Sometimes you want to listen in on a method and block its real behavior at the same time, for example because it connects to the network. Then you attach mockImplementation to spyOn. The replaced method returns a promise, so the test is async and waits for the result with await:

1test('spyOn - replacing implementation', async () => {
2  const api = {
3    fetchDinoData(id) {
4      // Normal API call - we don't want this in tests
5      return fetch(`/api/dinosaurs/${id}`);
6    }
7  };
8
9  // Replace implementation for the duration of the test
10  const spy = jest.spyOn(api, 'fetchDinoData').mockImplementation((id) => {
11    return Promise.resolve({
12      id,
13      name: 'Mock Rex',
14      species: 'T-Rex'
15    });
16  });
17
18  // Now fetchDinoData returns mock data
19  const data = await api.fetchDinoData('001');
20  expect(data.name).toBe('Mock Rex');
21
22  spy.mockRestore();
23});

If instead of await you wrote a bare .then(...) without return, the test would finish before the promise was fulfilled and would pass before the assertion even ran. We will take a close look at this trap in the next lesson.

Checking Mock Calls

Jest provides a rich set of matchers for mocks. Every mock also has a mock.calls property: an array in which each element is the list of arguments of one call:

1test('matchers for mocks', () => {
2  const callback = jest.fn();
3
4  callback('Rex');
5  callback('Blue', 'raptor');
6
7  // Was it called?
8  expect(callback).toHaveBeenCalled();
9
10  // How many times?
11  expect(callback).toHaveBeenCalledTimes(2);
12
13  // With what arguments?
14  expect(callback).toHaveBeenCalledWith('Rex');
15  expect(callback).toHaveBeenNthCalledWith(1, 'Rex');
16  expect(callback).toHaveBeenNthCalledWith(2, 'Blue', 'raptor');
17
18  // Access to call details
19  expect(callback.mock.calls).toHaveLength(2);
20  expect(callback.mock.calls[0]).toEqual(['Rex']);
21  expect(callback.mock.calls[1]).toEqual(['Blue', 'raptor']);
22});

toHaveBeenNthCalledWith numbers calls from 1, while mock.calls is an ordinary array indexed from 0, hence the different numbers in the same test. Matchers produce clearer error messages, so reach for mock.calls only in unusual situations.

Cleaning Up After Mocks

A mock remembers calls across tests, too. That is why in the alertService example the beforeEach hook calls mockClear, which clears the call history. mockReset does the same and also removes the configured behavior, and mockRestore additionally restores the original method after spyOn. For all mocks at once there are jest.clearAllMocks, jest.resetAllMocks and jest.restoreAllMocks, and the clearMocks: true configuration option clears the history automatically before every test.

My advice: mock the boundaries of the system, meaning the network, the database, the clock and SMS messages, not your own logic. If you replace everything, the test only checks that the mocks work. In the next lesson, mockResolvedValue meets asynchronous testing, and in the lab below you will see how a simplified jest.fn() works under the hood.

Remember: a mock is a dinosaur stand-in at a security drill - it looks real, it does not bite, and it records every move.

Code for this lesson: index.js
1// Mocking - jest.fn(), jest.mock(), spyOn
2console.log("=== Jurassic Park - Cloning and Mocking ===\n");
3
4// Simulating jest.fn()
5function createMockFn(impl) {
6  const mock = {
7    calls: [],
8    results: [],
9    returnValues: [],
10    implementations: impl ? [impl] : [],
11  };
12
13  function mockFn(...args) {
14    mock.calls.push(args);
15    let result;
16    if (mock.returnValues.length > 0) {
17      result = mock.returnValues.shift();
18    } else if (mock.implementations.length > 0) {
19      result = mock.implementations[0](...args);
20    } else {
21      result = undefined;
22    }
23    mock.results.push(result);
24    return result;
25  }
26
27  mockFn.mock = mock;
28  mockFn.mockReturnValue = (val) => {
29    mock.implementations = [() => val];
30    return mockFn;
31  };
32  mockFn.mockReturnValueOnce = (val) => {
33    mock.returnValues.push(val);
34    return mockFn;
35  };
36  mockFn.mockImplementation = (fn) => {
37    mock.implementations = [fn];
38    return mockFn;
39  };
40  mockFn.mockClear = () => {
41    mock.calls = [];
42    mock.results = [];
43  };
44
45  return mockFn;
46}
47
48// --- jest.fn() basics ---
49console.log("--- jest.fn() - creating a mock function ---\n");
50
51const feedDino = createMockFn();
52feedDino("Rex", "meat");
53feedDino("Brachio", "plants");
54
55console.log("Calls:", feedDino.mock.calls);
56console.log("Number of calls:", feedDino.mock.calls.length);
57console.log("First call:", feedDino.mock.calls[0]);
58console.log("Second call:", feedDino.mock.calls[1]);
59
60// --- Mock with a return value ---
61console.log("\n--- Mock with a return value ---\n");
62
63const getHealth = createMockFn();
64getHealth.mockReturnValue(100);
65console.log("getHealth():", getHealth());
66console.log("getHealth():", getHealth());
67
68// --- Mock with different values ---
69console.log("\n--- Mock with different values (mockReturnValueOnce) ---\n");
70
71const checkSensor = createMockFn();
72checkSensor.mockReturnValueOnce("OK");
73checkSensor.mockReturnValueOnce("WARNING");
74checkSensor.mockReturnValueOnce("DANGER");
75
76console.log("Reading 1:", checkSensor());
77console.log("Reading 2:", checkSensor());
78console.log("Reading 3:", checkSensor());
79
80// --- Mock with an implementation ---
81console.log("\n--- Mock with an implementation ---\n");
82
83const calculateDamage = createMockFn((attack, defense) => {
84  return Math.max(0, attack - defense);
85});
86
87console.log("damage(50, 30):", calculateDamage(50, 30));
88console.log("damage(10, 30):", calculateDamage(10, 30));
89console.log("Calls:", calculateDamage.mock.calls);
90
91// --- spyOn - spying on methods ---
92console.log("\n--- spyOn - monitoring existing methods ---\n");
93
94const park = {
95  dinosaurs: [],
96  addDinosaur(dino) {
97    this.dinosaurs.push(dino);
98    return this.dinosaurs.length;
99  }
100};
101
102// Simulating spyOn
103const originalAdd = park.addDinosaur.bind(park);
104const spyCalls = [];
105park.addDinosaur = function(...args) {
106  spyCalls.push(args);
107  return originalAdd(...args);
108};
109
110park.addDinosaur({ name: "Rex" });
111park.addDinosaur({ name: "Blue" });
112
113console.log("Spy calls:", spyCalls.length);
114console.log("First call:", JSON.stringify(spyCalls[0]));
115console.log("Dinosaurs in the park:", park.dinosaurs.length);
116console.log("(The original function still works!)");
117
118// --- Matchers for mocks ---
119console.log("\n--- Checking mock calls ---\n");
120const callback = createMockFn();
121callback("Rex");
122callback("Blue", "raptor");
123
124console.log("Was it called?", callback.mock.calls.length > 0 ? "YES" : "NO");
125console.log("How many times?", callback.mock.calls.length);
126console.log("All calls:", JSON.stringify(callback.mock.calls));

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does jest.fn() do in Jest?

  2. 2. What is the main difference between jest.fn() and jest.spyOn()?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Use mock functions to test the park's alarm system.

  • Horizontal ordering

    Arrange the elements of a mock return value configuration:

  • Code editor

    Monitor the park system's methods using spyOn and verify calls.

Useful articles