JavaScript and TypeScript course Β· Module 11: Testing with Jest
Matchers in Jest
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. What is the main difference between the toBe and toEqual matchers in Jest?
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.