JavaScript and React course Β· Module 13: Testing React

Jest - The Rocket Engine of Tests

8 min read
In this lesson9

Before every launch, engineers check hundreds of parameters: fuel pressure, engine temperature, communications. Nobody does this by hand after every change to the rocket, because it would take weeks. Code is no different: after every fix you want to know within seconds whether something broke, and you need a tool that runs all the checks at once.

Jest is a testing framework created by Facebook (Meta) that forms the foundation of React application testing. Since 2022 it has been developed under the OpenJS Foundation. Think of it as the main rocket engine - without it, no test gets off the ground. Jest packs everything into one package: a test runner, assertions, mocks and a coverage report.

Where Jest looks for tests

By default Jest finds files ending in .test.js or .spec.js (also .jsx, .ts, .tsx) in any folder of the project, plus every file inside __tests__ folders. A test usually sits next to its component, for example SpaceShip.test.js beside SpaceShip.js. Tests of single functions and components like these are unit tests: they sit at the base of the testing pyramid because they are the fastest and cheapest.

Test Structure - describe and it/test

We organize tests in describe blocks (describing a group of tests) and it or test (individual tests). describe groups related tests, and inside it every it checks one behavior using the expect function. In the example, createSpaceShip() creates a ship with a full tank (fuel equal to 100) and a travel method that burns fuel and throws a No fuel error when the tank is empty:

1describe('SpaceShip', () => {
2  it('should have full fuel at start', () => {
3    const ship = createSpaceShip();
4    expect(ship.fuel).toBe(100);
5  });
6
7  it('should consume fuel during travel', () => {
8    const ship = createSpaceShip();
9    ship.travel(50);
10    expect(ship.fuel).toBeLessThan(100);
11  });
12
13  test('should not travel without fuel', () => {
14    const ship = createSpaceShip();
15    ship.fuel = 0;
16    expect(() => ship.travel(10)).toThrow('No fuel');
17  });
18});

it and test are aliases - they do exactly the same thing. Convention says: it('should...') or test('returns...'). Every test creates its own ship, so tests share no state and can run in any order.

Matchers (Assertions) - Diagnostic Sensors

Matchers are functions that check whether the result is what we expect. Jest offers a rich set. The pattern is always the same: expect(value), then a dot and a matcher with the expectation.

Value Comparison

Most often you compare a result with an expected value. This is where the most important distinction in all of Jest hides:

1// Exact equality
2expect(2 + 2).toBe(4);
3
4// Object comparison (deep equality)
5expect({ name: 'Apollo' }).toEqual({ name: 'Apollo' });
6
7// Negation
8expect(fuel).not.toBe(0);

toBe compares with Object.is, so for two separate objects it returns false even when their contents are identical. For objects and arrays use toEqual, which checks the contents field by field.

Truthiness

The next group of matchers checks whether a value exists at all and how it behaves in an if condition:

1expect(null).toBeNull();
2expect(undefined).toBeUndefined();
3expect('Apollo').toBeDefined();
4expect(true).toBeTruthy();
5expect(0).toBeFalsy();

toBeTruthy and toBeFalsy are loose, because 0, an empty string and null are all falsy values. When you know the exact value, pick a precise matcher.

Numbers

Numbers have their own comparisons, handy for speed or fuel level:

1expect(speed).toBeGreaterThan(0);
2expect(fuel).toBeLessThanOrEqual(100);
3expect(0.1 + 0.2).toBeCloseTo(0.3);

toBeCloseTo saves you with fractions, because 0.1 + 0.2 in JavaScript is 0.30000000000000004, and toBe(0.3) would fail.

Strings

You can check text with a regular expression or by looking for a fragment:

1expect('Mission Control').toMatch(/mission/i);
2expect('Apollo 13').toContain('Apollo');

The i flag in the expression makes the check case-insensitive.

Arrays and Objects

Collections have matchers for checking contents, length and properties:

1expect(['Earth', 'Mars', 'Jupiter']).toContain('Mars');
2expect(crewMembers).toHaveLength(5);
3expect(spaceship).toHaveProperty('engine');
4expect(spaceship).toHaveProperty('crew.captain', 'Nova');

toHaveProperty accepts a dotted path, so it reaches the nested crew.captain field and compares its value.

Exceptions

When checking errors, remember to wrap the call in an arrow function:

1expect(() => launchWithoutFuel()).toThrow();
2expect(() => divideByZero()).toThrow('Division by zero');
3expect(() => invalidInput()).toThrow(ValidationError);

If you wrote expect(launchWithoutFuel()), the error would be thrown before expect runs and the test would crash instead of catching it.

Pure Functions - the Easiest Target for Tests

Before you start checking code, it pays to write it so that it is easy to check. Look at a function that burns fuel stored in a variable outside it:

1// Impure function: changes a variable outside itself
2let fuel = 100;
3
4function burnFuel(amount) {
5  fuel = fuel - amount;
6  return fuel;
7}

The result of burnFuel(10) depends on how many times the function was called before: the first time it returns 90, the second time 80. A test that expects 90 passes or fails depending on the order of the tests. A pure function does not have this problem: for the same arguments it always returns the same result and changes nothing outside itself. It handles invalid data predictably too, by throwing an error that a test catches with toThrow:

1// Pure function: the result depends only on the arguments
2function calculateTravelTime(distance, speed) {
3  if (speed <= 0) {
4    throw new Error('Speed must be positive');
5  }
6  return distance / speed;
7}
8
9test('calculates travel time', () => {
10  expect(calculateTravelTime(300, 100)).toBe(3);
11});
12
13test('throws when speed is not positive', () => {
14  expect(() => calculateTravelTime(300, 0)).toThrow('Speed must be positive');
15});

Each test is one call and one expect, with no setup and no cleanup. That is why you should move the logic you want to check, such as fuel calculations or coordinate validation, into separate pure functions, and let the component only display their result.

Setup and Teardown - Pre-Launch Preparation

Tests often require environment setup and cleanup. Jest provides four lifecycle functions for that:

1describe('MissionControl', () => {
2  let missionControl;
3
4  // Before EACH test
5  beforeEach(() => {
6    missionControl = new MissionControl();
7    missionControl.initialize();
8  });
9
10  // After EACH test
11  afterEach(() => {
12    missionControl.shutdown();
13  });
14
15  // Before ALL tests in the group
16  beforeAll(() => {
17    console.log('Starting mission control tests');
18  });
19
20  // After ALL tests in the group
21  afterAll(() => {
22    console.log('All tests completed');
23  });
24
25  it('should have zero active missions at start', () => {
26    expect(missionControl.activeMissions).toBe(0);
27  });
28});

My advice: prefer beforeEach, because every test gets a fresh object. Keep beforeAll for expensive operations, like starting a test server.

Asynchronous Testing in Jest

Many operations in space take time - same in code. Jest has to know that it must wait for the result:

1// Returning a Promise
2test('fetches crew data', () => {
3  return fetchCrewData().then(data => {
4    expect(data).toHaveLength(5);
5  });
6});
7
8// Async/await (preferred)
9test('fetches crew data', async () => {
10  const data = await fetchCrewData();
11  expect(data).toHaveLength(5);
12});
13
14// Testing rejected Promise
15test('throws on invalid mission', async () => {
16  await expect(fetchMission(-1)).rejects.toThrow('Invalid mission ID');
17});

Without return or await, the test would finish before the response arrives and pass without checking anything.

Jest Mock Functions - Flight Simulators

Mock functions allow you to simulate function behavior without actually calling them. jest.fn() creates a function that records every call:

1// Creating a mock function
2const mockLaunch = jest.fn();
3
4// Calling it
5mockLaunch('Apollo', 'Moon');
6
7// Checking
8expect(mockLaunch).toHaveBeenCalled();
9expect(mockLaunch).toHaveBeenCalledTimes(1);
10expect(mockLaunch).toHaveBeenCalledWith('Apollo', 'Moon');
11
12// Mock with return value
13const mockFuel = jest.fn().mockReturnValue(100);
14expect(mockFuel()).toBe(100);
15
16// Mock with implementation
17const mockCalculateRoute = jest.fn((from, to) => {
18  return { distance: 1000, estimatedTime: '2h' };
19});

The mock does not launch a real rocket, yet you can still check how many times and with what arguments it was called. More on mocking whole modules in a later lesson.

Running Tests

Jest offers several useful commands for running tests. You usually call them through the test script from package.json:

1# Run all tests
2npm test
3
4# Run tests in watch mode (automatically after changes)
5npm test -- --watch
6
7# Run tests from a specific file
8npm test -- SpaceShip.test.js
9
10# Run tests matching a pattern
11npm test -- --testPathPatterns="mission"
12
13# Run with code coverage report
14npm test -- --coverage

The double dash passes the options on to Jest. Since Jest 30, the path filtering option is called --testPathPatterns, and the older --testPathPattern ends with an error. Watch mode (--watch) runs only the tests related to changed files and detects the changes through git, so it works inside a repository. In a folder without git, Jest asks you to use --watchAll, which reruns all tests after every change.

Best Practices for Testing with Jest

  1. One behavior per test - each test should check one specific thing; several assertions about the same behavior are fine
  2. Descriptive names - it('should calculate fuel consumption for 100km trip') instead of it('test fuel')
  3. Arrange-Act-Assert - prepare data, perform action, check result
  4. Isolation - tests should not depend on each other or on execution order

In the next lesson you will combine Jest with React Testing Library and start testing components.

Jest is a powerful rocket engine for tests - mastering it will give you confidence that your application works correctly in all cosmic conditions!

Code for this lesson: App.jsx
1import React, { useState } from 'react';
2
3// ===== Functions to test =====
4
5function calculateFuelConsumption(distance, speed) {
6  if (speed <= 0) throw new Error('Speed must be positive');
7  return distance / speed * 1.5;
8}
9
10function sortByPriority(missions) {
11  return [...missions].sort((a, b) => a.priority - b.priority);
12}
13
14// ===== A miniature expect =====
15// The preview does not run Jest, so this function imitates three of its matchers.
16// Just like in Jest, a failed assertion throws an error.
17
18function expect(actual) {
19  return {
20    toBe(expected) {
21      if (Object.is(actual, expected)) return;
22      if (JSON.stringify(actual) === JSON.stringify(expected)) {
23        throw new Error('same contents, but two different objects - compare them with toEqual');
24      }
25      throw new Error('expected ' + JSON.stringify(expected) + ', received ' + JSON.stringify(actual));
26    },
27    toEqual(expected) {
28      if (JSON.stringify(actual) !== JSON.stringify(expected)) {
29        throw new Error('expected ' + JSON.stringify(expected) + ', received ' + JSON.stringify(actual));
30      }
31    },
32    toThrow(message) {
33      try {
34        actual();
35      } catch (error) {
36        if (error.message === message) return;
37        throw new Error('an error with a different message was thrown: ' + error.message);
38      }
39      throw new Error('the function did not throw');
40    },
41  };
42}
43
44function runTest(name, fn) {
45  try {
46    fn();
47    return { name, passed: true };
48  } catch (error) {
49    return { name, passed: false, details: error.message };
50  }
51}
52
53// ===== A Jest-style report =====
54
55function TestResult({ name, passed, details }) {
56  return (
57    <div className={'test-result ' + (passed ? 'pass' : 'fail')}>
58      <span className="icon">{passed ? '√' : 'Γ—'}</span>
59      <span className="name">{name}</span>
60      {details && <span className="details">{details}</span>}
61    </div>
62  );
63}
64
65function TestSuite({ title, tests }) {
66  const allPassed = tests.every((t) => t.passed);
67  return (
68    <div className="test-suite">
69      <h3>
70        <span className={'badge ' + (allPassed ? 'pass' : 'fail')}>
71          {allPassed ? 'PASS' : 'FAIL'}
72        </span>
73        {title}
74      </h3>
75      {tests.map((test) => (
76        <TestResult key={test.name} {...test} />
77      ))}
78    </div>
79  );
80}
81
82const MISSIONS = [
83  { name: 'Mars', priority: 2 },
84  { name: 'Moon', priority: 1 },
85];
86
87export default function App() {
88  const [matcher, setMatcher] = useState('toBe');
89  const other = matcher === 'toBe' ? 'toEqual' : 'toBe';
90
91  const fuelTests = [
92    runTest('calculates 3 units per 100 km at 50 km/s', () => {
93      expect(calculateFuelConsumption(100, 50)).toBe(3);
94    }),
95    runTest('returns 0 for a distance of 0', () => {
96      expect(calculateFuelConsumption(0, 50)).toBe(0);
97    }),
98    runTest('throws at a speed of 0', () => {
99      expect(() => calculateFuelConsumption(100, 0)).toThrow('Speed must be positive');
100    }),
101  ];
102
103  const sortTests = [
104    runTest('orders missions by priority (' + matcher + ')', () => {
105      expect(sortByPriority(MISSIONS))[matcher]([
106        { name: 'Moon', priority: 1 },
107        { name: 'Mars', priority: 2 },
108      ]);
109    }),
110  ];
111
112  const all = [...fuelTests, ...sortTests];
113  const passedCount = all.filter((t) => t.passed).length;
114
115  return (
116    <div className="app">
117      <h1>Jest - The Rocket Engine of Tests</h1>
118      <p>Below you can see the results of tests running "live" in the preview.</p>
119      <TestSuite title="calculateFuelConsumption" tests={fuelTests} />
120      <TestSuite title="sortByPriority" tests={sortTests} />
121      <button className="switch" onClick={() => setMatcher(other)}>
122        Switch {matcher} to {other}
123      </button>
124      <p className="hint">
125        toBe compares with Object.is, so it checks whether it is the same object.
126        sortByPriority returns a new array, so only toEqual can check its contents.
127      </p>
128      <p className="summary">
129        Passed: {passedCount}, failed: {all.length - passedCount}, total: {all.length}
130      </p>
131    </div>
132  );
133}

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. Which type of test sits at the base of the testing pyramid and is the fastest?

  2. 2. What is the describe() block used for in Jest?

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

Hands-on tasks in the game

  • Code editor

    Complete two pure navigation functions that the tests check (you see the results in the test panel, and the preview shows a mini PASS/FAIL report). ___BLANK1___: calculateFuelConsumption throws an error when the speed is not greater than zero (0 or a negative number). ___BLANK2___: the function returns the fuel consumption as in the lesson: distance divided by speed, times 1.5 (calculateFuelConsumption(100, 50) gives 3). ___BLANK3___: isValidCoordinate returns true only for a number from -1000 to 1000 inclusive, and false for everything else: numbers out of range, NaN, Infinity, strings such as '42', null and undefined.

  • Click in order

    Arrange a Jest assertion that checks whether result equals 42:

Useful articles