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

Matchers in Jest

7 min read
In this lesson6

"Is the result correct?" is often not enough. Sometimes you want to know whether the species list contains a raptor, whether health is within the norm, or whether a function raised an alarm. Jest has a separate tool for each of these questions.

Matchers are methods that allow you to check values in different ways. In the context of Jurassic Park - they are different types of sensors, each detecting a different kind of threat. You chain them with a dot after expect(...), and the selection rule is simple: pick the most precise sensor, because then Jest prints the clearest error message.

Basic Comparison Matchers

toBe - Strict Comparison (===)

toBe uses strict comparison. It checks identity - ideal for primitive types. More precisely: Jest compares values with Object.is, which works like === with two exceptions - NaN equals NaN, and 0 and -0 are told apart:

1test('toBe - comparing primitives', () => {
2  const dinoCount = 15;
3  expect(dinoCount).toBe(15);      // number
4
5  const species = 'T-Rex';
6  expect(species).toBe('T-Rex');    // string
7
8  const isAlive = true;
9  expect(isAlive).toBe(true);       // boolean
10});

Each of the three assertions compares a primitive: a number, a string and a boolean. For those, only the value itself matters, so toBe is the simplest choice here.

toEqual - Structural Comparison

toEqual checks structural equality of objects. It compares contents, not references. This matters because two objects with identical contents are two separate specimens in memory, so for toBe they will always be different:

1test('toEqual - comparing objects', () => {
2  const dino1 = { name: 'Rex', species: 'T-Rex' };
3  const dino2 = { name: 'Rex', species: 'T-Rex' };
4
5  // toBe FAIL - different references (objects in memory)
6  // expect(dino1).toBe(dino2); // This won't work!
7
8  // toEqual PASS - same contents
9  expect(dino1).toEqual(dino2); // This works!
10
11  // Also works with nested objects
12  const enclosure = {
13    name: 'Zone A',
14    dinosaurs: [
15      { name: 'Rex', diet: 'carnivore' }
16    ]
17  };
18
19  expect(enclosure).toEqual({
20    name: 'Zone A',
21    dinosaurs: [
22      { name: 'Rex', diet: 'carnivore' }
23    ]
24  });
25});

toEqual goes recursively into nested objects and arrays, field by field. It is lenient, though: it ignores properties whose value is undefined and does not check whether an object is an instance of the same class. When those details matter, use toStrictEqual - I reach for it whenever I have no reason to be lenient.

Matchers for Arrays and Strings

toContain - Does It Contain an Element?

When you do not care about the whole list, only about the presence of one specimen, use toContain. It works with both arrays and strings:

1test('toContain - checking contents', () => {
2  const dinosaurs = ['T-Rex', 'Velociraptor', 'Triceratops'];
3
4  expect(dinosaurs).toContain('T-Rex');
5  expect(dinosaurs).toContain('Velociraptor');
6
7  // For strings
8  const report = 'Dinosaur T-Rex in zone A';
9  expect(report).toContain('T-Rex');
10  expect(report).toContain('zone A');
11});

For an array, toContain checks whether some element is strictly equal to the one you are looking for, and for a string, whether it contains the given fragment. Careful with objects: toContain compares references, so to find an object by its contents use toContainEqual.

toHaveLength - Checking Length

The number of specimens in an enclosure is another common question. toHaveLength checks the length property, so it works with arrays and strings:

1test('toHaveLength - checking count', () => {
2  const carnivores = ['T-Rex', 'Velociraptor', 'Spinosaurus'];
3  expect(carnivores).toHaveLength(3);
4
5  const emptyEnclosure = [];
6  expect(emptyEnclosure).toHaveLength(0);
7
8  // Also works with strings
9  const code = 'DINO-001';
10  expect(code).toHaveLength(8);
11});

Writing expect(carnivores).toHaveLength(3) gives the same result as expect(carnivores.length).toBe(3), but on failure Jest shows the whole array, not just a number. An empty array has a length of 0, and that is a valid check too.

Matchers for Truthy/Falsy Values

toBeTruthy and toBeFalsy

In JavaScript, every value in an if condition is converted to true or false. toBeTruthy and toBeFalsy check the result of exactly that conversion, not a specific value:

1test('checking truthiness', () => {
2  // Truthy values
3  expect(1).toBeTruthy();
4  expect('text').toBeTruthy();
5  expect([]).toBeTruthy();      // empty array is truthy!
6  expect({}).toBeTruthy();      // empty object is truthy!
7
8  // Falsy values
9  expect(0).toBeFalsy();
10  expect('').toBeFalsy();
11  expect(null).toBeFalsy();
12  expect(undefined).toBeFalsy();
13  expect(false).toBeFalsy();
14});

The trap is in the first group: an empty array and an empty object are truthy. If you want to check that an enclosure is empty, toBeFalsy will not help - use toHaveLength(0).

toBeNull, toBeUndefined, toBeDefined

Let's assume getDinosaur returns a dinosaur object, or null when the specimen is not in the registry. Three matchers tell apart three different states of a missing value:

1test('checking null and undefined', () => {
2  const activeDino = getDinosaur('Rex-001');
3  const extinctDino = getDinosaur('Unknown');
4
5  expect(activeDino).toBeDefined();
6  expect(activeDino).not.toBeNull();
7
8  expect(extinctDino).toBeNull();
9  expect(extinctDino).toBeDefined(); // null is defined!
10
11  let uninitializedDino;
12  expect(uninitializedDino).toBeUndefined();
13});

toBeDefined only means "not undefined", which is why null passes this test. If you want to rule out both empty states at once, check for a concrete value instead of relying on toBeDefined.

Numeric Matchers

The park's sensors return numbers: health, fence voltage, incubator temperature. For comparisons there are matchers whose names read like plain English sentences:

1test('numeric comparisons', () => {
2  const health = 85;
3
4  expect(health).toBeGreaterThan(80);
5  expect(health).toBeGreaterThanOrEqual(85);
6  expect(health).toBeLessThan(100);
7  expect(health).toBeLessThanOrEqual(85);
8
9  // For floating point numbers - DO NOT use toBe!
10  expect(0.1 + 0.2).toBeCloseTo(0.3);
11});

The last line is an important lesson about floating-point numbers: 0.1 + 0.2 gives 0.30000000000000004 in JavaScript, so toBe(0.3) would fail. By default, toBeCloseTo compares with a precision of two decimal places.

Testing Exceptions - toThrow

toThrow checks whether a function throws an exception. NOTE: you must wrap the call in a function! Without the wrapper, the error explodes before expect has a chance to catch it. In the example, releaseDinosaur releases a specimen only at an adequate security level:

1function releaseDinosaur(dinosaur, securityLevel) {
2  if (securityLevel < 5) {
3    throw new Error('Security level too low!');
4  }
5  if (dinosaur.species === 'T-Rex' && securityLevel < 9) {
6    throw new Error('T-Rex requires security level 9!');
7  }
8  return { released: true };
9}
10
11test('toThrow - testing exceptions', () => {
12  const rex = { name: 'Rex', species: 'T-Rex' };
13
14  // CORRECT - wrap in an arrow function
15  expect(() => releaseDinosaur(rex, 3)).toThrow();
16  expect(() => releaseDinosaur(rex, 3)).toThrow('Security level too low!');
17  expect(() => releaseDinosaur(rex, 7)).toThrow('T-Rex requires security level 9!');
18
19  // Check that it does NOT throw an exception
20  expect(() => releaseDinosaur(rex, 9)).not.toThrow();
21});

The argument of toThrow narrows the check: a string means the error message must contain that fragment, a regular expression means it must match it, and an error class means the exception must be an instance of it. I recommend always passing the message because a bare toThrow() lets any exception through, including a TypeError caused by a typo.

The not Modifier

Every matcher can be negated using .not. It comes in handy when you want to confirm an absence, for example that a stegosaurus did not end up in the predator enclosure:

1test('the not modifier', () => {
2  const dinosaurs = ['T-Rex', 'Triceratops'];
3
4  expect(dinosaurs).not.toContain('Stegosaurus');
5  expect(dinosaurs.length).not.toBe(0);
6  expect(dinosaurs).not.toEqual([]);
7
8  const health = 85;
9  expect(health).not.toBeLessThan(50);
10});

not reverses the result of any matcher, but do not overuse it: not.toBeLessThan(50) is harder to read than toBeGreaterThanOrEqual(50). Reach for not where the sentence "does not contain" sounds natural.

In the lesson on mocking you will meet matchers that check function calls, and in the TypeScript lesson, asymmetric matchers that verify just the shape of an object. In the lab below you will test a simplified version of expect with the same matchers.

Remember: a matcher is a sensor - pick the one that detects exactly the threat you are looking for.

Code for this lesson: index.js
1// Matchers in Jest - different kinds of checks
2console.log("=== Jurassic Park - Sensors and Matchers ===\n");
3
4// Mini expect with many matchers
5let passed = 0, failed = 0;
6
7function expect(actual) {
8  function check(condition, msg) {
9    if (condition) { passed++; console.log("  [PASS] " + msg); }
10    else { failed++; console.log("  [FAIL] " + msg); }
11  }
12
13  return {
14    toBe(expected) {
15      check(actual === expected, `${actual} === ${expected}`);
16    },
17    toEqual(expected) {
18      check(JSON.stringify(actual) === JSON.stringify(expected),
19        `deepEqual: ${JSON.stringify(actual)}`);
20    },
21    toContain(item) {
22      const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
23      check(has, `contains "${item}"`);
24    },
25    toHaveLength(len) {
26      check(actual.length === len, `length ${actual.length} === ${len}`);
27    },
28    toBeTruthy() {
29      check(!!actual, `${actual} is truthy`);
30    },
31    toBeFalsy() {
32      check(!actual, `${actual} is falsy`);
33    },
34    toBeGreaterThan(n) {
35      check(actual > n, `${actual} > ${n}`);
36    },
37    toBeLessThan(n) {
38      check(actual < n, `${actual} < ${n}`);
39    },
40    toBeCloseTo(expected) {
41      check(Math.abs(actual - expected) < 0.01, `${actual} ~= ${expected}`);
42    },
43    toBeNull() {
44      check(actual === null, `${actual} is null`);
45    },
46    toBeUndefined() {
47      check(actual === undefined, `is undefined`);
48    },
49    toBeDefined() {
50      check(actual !== undefined, `is defined`);
51    },
52    not: {
53      toBe(expected) { check(actual !== expected, `${actual} !== ${expected}`); },
54      toContain(item) {
55        const has = Array.isArray(actual) ? actual.includes(item) : actual.includes(item);
56        check(!has, `not contains "${item}"`);
57      },
58      toThrow() { /* simplified */ }
59    }
60  };
61}
62
63// --- toBe vs toEqual ---
64console.log("--- toBe vs toEqual ---");
65
66const dino1 = { name: "Rex", species: "T-Rex" };
67const dino2 = { name: "Rex", species: "T-Rex" };
68
69console.log("toBe compares references (===):");
70console.log("  dino1 === dino2?", dino1 === dino2, "(false - different objects)");
71console.log("\ntoEqual compares contents:");
72expect(dino1).toEqual(dino2);
73
74// --- toContain ---
75console.log("\n--- toContain ---");
76const dinosaurs = ["T-Rex", "Velociraptor", "Triceratops"];
77expect(dinosaurs).toContain("T-Rex");
78expect(dinosaurs).not.toContain("Stegosaurus");
79
80const report = "Dinosaur T-Rex in zone A";
81expect(report).toContain("T-Rex");
82
83// --- toHaveLength ---
84console.log("\n--- toHaveLength ---");
85const carnivores = ["T-Rex", "Velociraptor", "Spinosaurus"];
86expect(carnivores).toHaveLength(3);
87
88// --- Truthy / Falsy ---
89console.log("\n--- Truthy / Falsy ---");
90expect(1).toBeTruthy();
91expect("text").toBeTruthy();
92expect(0).toBeFalsy();
93expect("").toBeFalsy();
94expect(null).toBeFalsy();
95
96// --- Numbers ---
97console.log("\n--- Numeric comparisons ---");
98const health = 85;
99expect(health).toBeGreaterThan(80);
100expect(health).toBeLessThan(100);
101expect(0.1 + 0.2).toBeCloseTo(0.3);
102
103// --- null / undefined ---
104console.log("\n--- null / undefined ---");
105expect(null).toBeNull();
106expect(undefined).toBeUndefined();
107expect("value").toBeDefined();
108
109// --- toThrow ---
110console.log("\n--- toThrow ---");
111function releaseDinosaur(dino, level) {
112  if (level < 5) throw new Error("Security level too low!");
113  return { released: true };
114}
115
116try {
117  releaseDinosaur({}, 3);
118  console.log("  [FAIL] Should have thrown");
119  failed++;
120} catch (e) {
121  console.log("  [PASS] Threw: " + e.message);
122  passed++;
123}
124
125try {
126  releaseDinosaur({}, 9);
127  console.log("  [PASS] No throw for level 9");
128  passed++;
129} catch (e) {
130  console.log("  [FAIL] Should not throw");
131  failed++;
132}
133
134console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);

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 is the main difference between the toBe and toEqual matchers in Jest?

  2. 2. How do you correctly use the toThrow matcher 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

    For each test, use the correct matcher: toBe, toContain, toHaveLength, toBeGreaterThan, toEqual.

  • Horizontal ordering

    Arrange the elements of an assertion comparing an object:

  • Code editor

    Write tests checking both positive and negative conditions.

Useful articles