What is a matcher?
A matcher is the method you call on the result of expect(). It decides what counts as a match and what message to show when there is none. toBe is one matcher. Vitest and Jest each ship with dozens more, and they share almost all of the same names, so a test written with one usually runs on the other unchanged.
Choosing the matcher is choosing what the test promises. expect(total).toBe(6.6) promises an exact number. expect(names).toContain('Bun') promises only that a bun is somewhere in the list, in any position, next to anything else. The narrower promise is easier to keep when the code changes for good reasons, and the wider one catches more bugs. Part of writing tests well is picking the level that matches the rule you are checking.
Why does toBe fail on two identical objects?
toBe uses Object.is, which for objects and arrays asks whether both sides are the very same object in memory. summary() builds a brand new object on every call, and the literal in the test is another brand new object. They have the same contents and are still two different objects, so toBe fails every time. Results shows Expected and Received looking identical, and the message adds that the contents match but the objects differ. Real Vitest gives a similar hint and suggests toStrictEqual.
toEqual compares contents instead. It walks through every property and every array item and checks that they match. Use toBe for numbers, strings, booleans, null and undefined, and when you really mean one specific object. Use toEqual for objects and arrays you built from data.
const s = summary(basket);
expect(s.names).toEqual(['Croissant', 'Bun']); // same contents: passes
expect(s.names).toBe(['Croissant', 'Bun']); // different arrays: fails![s.names and the literal ['Croissant', 'Bun'] are two separate arrays with the same contents, so toEqual passes and toBe, which asks for the same object, fails.](/assets/diagrams/testing-expect-and-matchers-toequal-versus-tobe-arrays.webp?v=3.15.1)
![s.names and the literal ['Croissant', 'Bun'] are two separate arrays with the same contents, so toEqual passes and toBe, which asks for the same object, fails.](/assets/diagrams/testing-expect-and-matchers-toequal-versus-tobe-arrays-dark.webp?v=3.15.1)
Which matchers suit lists?
toEqual(list) checks the whole list, in order. That is the right choice when order counts, such as items in the order the customer added them. If you only care that something is present, toContain('Bun') checks for one item anywhere in the array, using the same comparison as toBe. For an array of objects, use toContainEqual({ name: 'Bun', qty: 1 }), which compares contents the way toEqual does.
toHaveLength(2) checks how many items there are. It works on strings too, and the failure message gives the real length, which reads better than expect(list.length).toBe(2) because the report names the list it checked. A count test is weak on its own: two wrong names still make a length of two. Pair it with a check on the contents.
Why do money totals need toBeCloseTo?
JavaScript stores numbers in binary floating point, and most decimal fractions cannot be stored exactly. 2.1 plus 1.2 comes out as 3.3000000000000003. Your code is fine, the sum is as close as the format allows, and toBe(3.3) still fails because the two numbers differ in the sixteenth decimal place.
toBeCloseTo(3.3) passes when the difference is less than half of 0.01, which means the values agree to two decimal places. Pass a second argument for a different precision: toBeCloseTo(3.3, 5) asks for five places. For prices, two places is exactly right. If the code rounds to pence before returning, as the total() in the first lesson did, toBe works again and is the stricter choice.
Inside an object, expect.closeTo(3.3) does the same job: expect(s).toEqual({ count: 2, total: expect.closeTo(3.3), names: ['Croissant', 'Bun'] }) checks the whole summary in one go.
How do you check text?
For a string that must be exact, toBe is best: expect(receiptLine(item)).toBe('3 x Croissant @ 2.10'). When you only care about part of the text, toMatch takes either a string, which must appear somewhere, or a regular expression. toMatch(/@ 2\.10$/) checks that the line ends with the price, written with two decimals, and does not care how the start is worded.
Pick test data that would expose a mistake. A price of 4.5 tests toFixed(2) well because it must turn into 4.50. A price of 4.25 would look the same whether the code used two decimals or, wrongly, kept the number as it was. The Check my work button tries toFixed(1) against your tests for exactly this reason.
test('a receipt line shows the price with two decimals', () => {
const line = receiptLine({ name: 'Croissant', price: 2.1, qty: 3 });
expect(line).toMatch(/^3 x Croissant @ 2\.10$/);
});When is toBeTruthy a trap?
toBeTruthy() passes for any value that counts as true in an if: every object, every non-empty string, every number except 0 and NaN. That makes it very easy to pass. In the Break It challenge, findMember(members, 'Ana') returns Ada, the wrong person, and toBeTruthy() is happy because Ada is an object.
Use it only when the rule really is about truthiness, which is rare. If a function returns true or false, write toBe(true), so a function that returns the string 'yes' by mistake gets caught. If it returns an object, check the object. toBeFalsy() has the same weakness the other way: 0, '', null and undefined all pass it.
How does .not work, and where does it mislead?
Putting .not before any matcher flips it. expect(names).not.toContain('Baguette') passes when the list has no baguette. It is handy for rules that say something must be absent, such as a sold-out item disappearing from the menu.
A negated check rules out one value and allows every other. expect(findMember(members, 'Zed')).not.toBeNull() sounds like a check that nobody was found, and it passes for undefined, for a member and for anything else that isn't null. When you know what the answer should be, assert that answer directly: toBeUndefined(). Keep .not for cases where the rule itself is a negative.
What do AI-written tests get wrong with matchers?
The most common slip is toBe on objects and arrays, which fails at once and gets patched by turning the expected value into whatever the code printed. Close behind is toBeTruthy() or toBeDefined() used as a lazy stand-in for a real value: tests that cannot fail look green and prove nothing.
Watch for toBe on computed prices too. A generated test may pick numbers that happen to add up exactly, like 2.5 and 1.5, and the test passes until someone changes the price list to 2.1 and 1.2. When the value is money calculated with decimals, reach for toBeCloseTo or make the code round.
Field notes
- Use
toBefor numbers, strings and booleans, andtoEqualfor objects and arrays. toContainchecks one item is in a list, andtoHaveLengthchecks the count.- Decimal sums can be off by a tiny amount, so use
toBeCloseTofor money. toBeTruthy()passes for almost anything, so prefer a real expected value.
Easy to get wrong
toBe compares object identity, so a freshly built object never matches. toEqual compares contents, and toBeCloseTo absorbs floating-point error in the total.
Any member at all is truthy, so the weak version passes when the wrong person comes back. Say exactly which answer you expect.
Decimal sums are rarely exact in JavaScript. Compare money to two decimal places, or round in the code and keep toBe.
Questions people ask
What is the difference between toEqual and toStrictEqual?
toStrictEqual also checks that undefined properties match and that both objects are the same class. toEqual treats { a: 1, b: undefined } and { a: 1 } as equal. The objects lesson covers when the stricter one is worth it.
Can I write my own matcher?
Yes, with expect.extend in both Vitest and Jest. The practice runner does not support it, and you rarely need one while learning. The built-in matchers cover nearly every check in this course.
Why does toBeCloseTo use half of 0.01?
Two values agree to two decimal places when they round to the same pence. A difference under 0.005 guarantees that. Real Vitest and Jest use the same rule, and the practice runner matches it.
Does toContain work on strings?
Yes. expect('Sourdough loaf').toContain('dough') passes. For text, toMatch is often more flexible, because it also accepts a regular expression.