Day 3: Mastering Matchers
What You'll Learn Today
- The difference between
toBeandtoEqual - Truthiness matchers (
toBeTruthy,toBeFalsy,toBeNull, etc.) - Number matchers (
toBeGreaterThan,toBeCloseTo, etc.) - String matchers (
toMatch,toContain) - Array and object matchers
- Exception matchers (
toThrow) - Negation with
not
What Are Matchers?
Matchers are verification methods chained after expect(). The toBe you used in Day 1 is one matcher.
expect(actual).matcher(expected);
flowchart LR
subgraph MatcherFlow["Matcher Flow"]
E["expect(actual value)"]
M["matcher(expected value)"]
R["Pass or Fail"]
end
E --> M --> R
style E fill:#8b5cf6,color:#fff
style M fill:#3b82f6,color:#fff
style R fill:#22c55e,color:#fff
Jest provides many matchers for different purposes. Choosing the right matcher makes your test intent clear and produces better error messages.
Equality: toBe vs toEqual
toBe β Strict Equality (===)
toBe uses JavaScript's === strict equality. Use it for primitive values.
test('toBe compares with strict equality', () => {
expect(1 + 2).toBe(3);
expect('hello').toBe('hello');
expect(true).toBe(true);
expect(null).toBe(null);
expect(undefined).toBe(undefined);
});
toEqual β Deep Equality
toEqual recursively compares the contents of objects and arrays.
test('toEqual compares object contents', () => {
const user = { name: 'Alice', age: 25 };
// β
toEqual: compares contents
expect(user).toEqual({ name: 'Alice', age: 25 });
// β toBe: fails because they are different object references
// expect(user).toBe({ name: 'Alice', age: 25 });
});
test('toEqual works with nested objects', () => {
const data = {
user: { name: 'Alice' },
scores: [90, 85, 92],
};
expect(data).toEqual({
user: { name: 'Alice' },
scores: [90, 85, 92],
});
});
toStrictEqual β Stricter Deep Equality
toStrictEqual also checks for undefined properties and object classes.
test('toStrictEqual checks for undefined properties', () => {
// toEqual: passes (undefined properties are ignored)
expect({ name: 'Alice', age: undefined }).toEqual({ name: 'Alice' });
// toStrictEqual: fails (undefined property exists)
// expect({ name: 'Alice', age: undefined }).toStrictEqual({ name: 'Alice' });
});
| Matcher | Comparison | Use For |
|---|---|---|
toBe |
=== (reference equality) |
Primitives (numbers, strings, booleans) |
toEqual |
Deep equality | Object and array contents |
toStrictEqual |
Stricter deep equality | When undefined properties matter |
flowchart TB
subgraph Comparison["Choosing an Equality Matcher"]
Q1{"What are you comparing?"}
P["Primitives\n(numbers, strings, booleans)"]
O["Objects / Arrays"]
S{"Check undefined\nproperties?"}
TBE["toBe"]
TEQ["toEqual"]
TSE["toStrictEqual"]
end
Q1 -->|Primitive| P --> TBE
Q1 -->|Object/Array| O --> S
S -->|No| TEQ
S -->|Yes| TSE
style TBE fill:#3b82f6,color:#fff
style TEQ fill:#8b5cf6,color:#fff
style TSE fill:#22c55e,color:#fff
Truthiness Matchers
These matchers test JavaScript's "truthy / falsy" values.
describe('truthiness matchers', () => {
test('toBeNull matches only null', () => {
expect(null).toBeNull();
});
test('toBeUndefined matches only undefined', () => {
expect(undefined).toBeUndefined();
});
test('toBeDefined matches anything that is not undefined', () => {
expect('hello').toBeDefined();
expect(0).toBeDefined();
expect(null).toBeDefined();
});
test('toBeTruthy matches truthy values', () => {
expect('hello').toBeTruthy();
expect(1).toBeTruthy();
expect([]).toBeTruthy();
expect({}).toBeTruthy();
});
test('toBeFalsy matches falsy values', () => {
expect(0).toBeFalsy();
expect('').toBeFalsy();
expect(null).toBeFalsy();
expect(undefined).toBeFalsy();
expect(false).toBeFalsy();
expect(NaN).toBeFalsy();
});
});
| Matcher | Matches |
|---|---|
toBeNull() |
Only null |
toBeUndefined() |
Only undefined |
toBeDefined() |
Anything except undefined |
toBeTruthy() |
Values that evaluate to true in an if |
toBeFalsy() |
Values that evaluate to false in an if |
Recap: JavaScript's falsy values are
false,0,"",null,undefined, andNaN(see "Learn JavaScript in 10 Days" Day 4).
Number Matchers
Matchers for comparing numbers and handling floating-point precision.
describe('number matchers', () => {
test('comparison matchers', () => {
const value = 10;
expect(value).toBeGreaterThan(9);
expect(value).toBeGreaterThanOrEqual(10);
expect(value).toBeLessThan(11);
expect(value).toBeLessThanOrEqual(10);
});
test('toBeCloseTo for floating point', () => {
// β floating point issue
// expect(0.1 + 0.2).toBe(0.3);
// β
use toBeCloseTo for floating point comparison
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 decimal places
});
});
TypeScript version:
describe('number matchers', () => {
test('comparison matchers', () => {
const value: number = 10;
expect(value).toBeGreaterThan(9);
expect(value).toBeGreaterThanOrEqual(10);
expect(value).toBeLessThan(11);
expect(value).toBeLessThanOrEqual(10);
});
test('toBeCloseTo for floating point', () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
});
| Matcher | Meaning |
|---|---|
toBeGreaterThan(n) |
> n |
toBeGreaterThanOrEqual(n) |
>= n |
toBeLessThan(n) |
< n |
toBeLessThanOrEqual(n) |
<= n |
toBeCloseTo(n, digits?) |
Approximate floating-point comparison |
Why
toBeCloseTo? In JavaScript,0.1 + 0.2equals0.30000000000000004. Always usetoBeCloseToinstead oftoBefor floating-point results.
String Matchers
describe('string matchers', () => {
test('toMatch with regular expression', () => {
expect('hello world').toMatch(/world/);
expect('hello world').toMatch(/^hello/);
expect('user@example.com').toMatch(/^[\w.]+@[\w.]+\.\w+$/);
});
test('toMatch with string', () => {
expect('hello world').toMatch('world');
expect('JavaScript is awesome').toMatch('awesome');
});
test('toContain with strings', () => {
expect('hello world').toContain('world');
});
test('toHaveLength for string length', () => {
expect('hello').toHaveLength(5);
expect('').toHaveLength(0);
});
});
| Matcher | Use For |
|---|---|
toMatch(regexp | string) |
Match a regex or substring |
toContain(string) |
Contains a substring |
toHaveLength(n) |
Verify string length |
Array Matchers
describe('array matchers', () => {
const fruits = ['apple', 'banana', 'cherry'];
test('toContain checks if array includes an item', () => {
expect(fruits).toContain('banana');
});
test('toHaveLength checks array length', () => {
expect(fruits).toHaveLength(3);
});
test('toEqual compares array contents', () => {
expect(fruits).toEqual(['apple', 'banana', 'cherry']);
});
test('toContainEqual checks object in array', () => {
const users = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];
expect(users).toContainEqual({ name: 'Alice', age: 25 });
});
});
TypeScript version:
describe('array matchers', () => {
const fruits: string[] = ['apple', 'banana', 'cherry'];
test('toContain checks if array includes an item', () => {
expect(fruits).toContain('banana');
});
test('toHaveLength checks array length', () => {
expect(fruits).toHaveLength(3);
});
test('toContainEqual checks object in array', () => {
const users: Array<{ name: string; age: number }> = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];
expect(users).toContainEqual({ name: 'Alice', age: 25 });
});
});
| Matcher | Use For |
|---|---|
toContain(item) |
Array includes a primitive value |
toContainEqual(obj) |
Array includes an object (deep equality) |
toHaveLength(n) |
Array length |
toEqual(arr) |
Compare entire array contents |
Object Matchers
describe('object matchers', () => {
const user = {
name: 'Alice',
age: 25,
email: 'alice@example.com',
address: {
city: 'Tokyo',
country: 'Japan',
},
};
test('toHaveProperty checks for a property', () => {
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('name', 'Alice');
});
test('toHaveProperty with nested path', () => {
expect(user).toHaveProperty('address.city', 'Tokyo');
});
test('toMatchObject checks partial match', () => {
expect(user).toMatchObject({
name: 'Alice',
age: 25,
});
// email and address are not checked
});
test('toEqual checks full match', () => {
expect(user).toEqual({
name: 'Alice',
age: 25,
email: 'alice@example.com',
address: {
city: 'Tokyo',
country: 'Japan',
},
});
});
});
| Matcher | Use For |
|---|---|
toHaveProperty(key, value?) |
Check a specific property exists (optionally with value) |
toMatchObject(obj) |
Partial match of object properties |
toEqual(obj) |
Full match of all properties |
flowchart TB
subgraph ObjMatch["Choosing an Object Matcher"]
Q{"What to verify?"}
PROP["A specific property"]
PARTIAL["Some properties"]
FULL["All properties"]
HP["toHaveProperty"]
MO["toMatchObject"]
EQ["toEqual"]
end
Q -->|"One property"| PROP --> HP
Q -->|"Partial check"| PARTIAL --> MO
Q -->|"Full comparison"| FULL --> EQ
style HP fill:#3b82f6,color:#fff
style MO fill:#8b5cf6,color:#fff
style EQ fill:#22c55e,color:#fff
Exception Matchers
Use toThrow to verify that a function throws an error.
function validateAge(age) {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0 || age > 150) {
throw new RangeError('Age must be between 0 and 150');
}
return true;
}
TypeScript version:
function validateAge(age: unknown): boolean {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0 || age > 150) {
throw new RangeError('Age must be between 0 and 150');
}
return true;
}
describe('toThrow', () => {
test('throws on non-number input', () => {
expect(() => validateAge('twenty')).toThrow();
});
test('throws TypeError with specific message', () => {
expect(() => validateAge('twenty')).toThrow('Age must be a number');
});
test('throws specific error type', () => {
expect(() => validateAge('twenty')).toThrow(TypeError);
});
test('throws RangeError for out-of-range values', () => {
expect(() => validateAge(-1)).toThrow(RangeError);
expect(() => validateAge(200)).toThrow('Age must be between 0 and 150');
});
test('does not throw for valid input', () => {
expect(() => validateAge(25)).not.toThrow();
});
});
Important: When using
toThrow, you must wrap the function call in an arrow function. Calling it directly would break the test itself.
// β wrong: error breaks the test
expect(validateAge('twenty')).toThrow();
// β
correct: wrap in a function
expect(() => validateAge('twenty')).toThrow();
| toThrow Usage | What It Checks |
|---|---|
toThrow() |
Throws any error |
toThrow('message') |
Error contains specific message |
toThrow(ErrorType) |
Error is of specific type |
toThrow(/regex/) |
Error message matches regex |
Negation with not
Every matcher can be negated by prepending .not.
describe('not modifier', () => {
test('not.toBe', () => {
expect(1 + 1).not.toBe(3);
});
test('not.toEqual', () => {
expect({ name: 'Alice' }).not.toEqual({ name: 'Bob' });
});
test('not.toContain', () => {
expect(['apple', 'banana']).not.toContain('cherry');
});
test('not.toBeNull', () => {
expect('hello').not.toBeNull();
});
test('not.toThrow', () => {
expect(() => validateAge(25)).not.toThrow();
});
});
expect.any and expect.anything
Use these to verify values by type or existence rather than exact value.
describe('asymmetric matchers', () => {
test('expect.any matches any value of a given type', () => {
const user = { name: 'Alice', age: 25, createdAt: new Date() };
expect(user).toEqual({
name: expect.any(String),
age: expect.any(Number),
createdAt: expect.any(Date),
});
});
test('expect.anything matches anything except null/undefined', () => {
const response = { id: 1, data: 'some value' };
expect(response).toEqual({
id: expect.anything(),
data: expect.anything(),
});
});
test('expect.stringContaining', () => {
expect('hello world').toEqual(expect.stringContaining('world'));
});
test('expect.arrayContaining', () => {
const arr = [1, 2, 3, 4, 5];
expect(arr).toEqual(expect.arrayContaining([1, 3, 5]));
});
test('expect.objectContaining', () => {
const user = { name: 'Alice', age: 25, email: 'alice@example.com' };
expect(user).toEqual(expect.objectContaining({
name: 'Alice',
age: 25,
}));
});
});
TypeScript version:
describe('asymmetric matchers', () => {
test('expect.any matches any value of a given type', () => {
const user = { name: 'Alice', age: 25, createdAt: new Date() };
expect(user).toEqual({
name: expect.any(String),
age: expect.any(Number),
createdAt: expect.any(Date),
});
});
test('expect.objectContaining', () => {
const user = { name: 'Alice', age: 25, email: 'alice@example.com' };
expect(user).toEqual(expect.objectContaining({
name: 'Alice',
age: 25,
}));
});
});
| Asymmetric Matcher | Use For |
|---|---|
expect.any(Constructor) |
Type-only check (String, Number, Date, etc.) |
expect.anything() |
Anything except null and undefined |
expect.stringContaining(str) |
String contains substring |
expect.stringMatching(regexp) |
String matches regex |
expect.arrayContaining(arr) |
Array includes specified elements (any order) |
expect.objectContaining(obj) |
Object includes specified properties |
Matcher Quick Reference
flowchart TB
subgraph Guide["Matcher Selection Guide"]
START{"What to verify?"}
PRIM["Primitive values"]
OBJ["Objects / Arrays"]
STR["String patterns"]
NUM["Number ranges"]
ERR["Errors"]
TRUTH["Truthiness"]
end
START --> PRIM --> |toBe| R1["toBe(value)"]
START --> OBJ --> |contents| R2["toEqual / toMatchObject"]
START --> STR --> |regex| R3["toMatch(regex)"]
START --> NUM --> |comparison| R4["toBeGreaterThan, etc."]
START --> ERR --> |exceptions| R5["toThrow()"]
START --> TRUTH --> |null/undefined| R6["toBeNull, etc."]
style R1 fill:#3b82f6,color:#fff
style R2 fill:#8b5cf6,color:#fff
style R3 fill:#22c55e,color:#fff
style R4 fill:#f59e0b,color:#fff
style R5 fill:#ef4444,color:#fff
style R6 fill:#3b82f6,color:#fff
Summary
| Category | Matchers | Use For |
|---|---|---|
| Equality | toBe, toEqual, toStrictEqual |
Value and object comparison |
| Truthiness | toBeTruthy, toBeFalsy, toBeNull |
Boolean and existence checks |
| Numbers | toBeGreaterThan, toBeCloseTo |
Range and approximation |
| Strings | toMatch, toContain |
Pattern and substring matching |
| Arrays | toContain, toContainEqual, toHaveLength |
Element and length checks |
| Objects | toHaveProperty, toMatchObject |
Property and partial matching |
| Exceptions | toThrow |
Error verification |
| Negation | not.matcher() |
Negate any matcher |
| Asymmetric | expect.any(), expect.objectContaining() |
Flexible partial matching |
Key Takeaways
- Use
toBefor primitives,toEqualfor objects and arrays - Always use
toBeCloseTofor floating-point comparisons - Wrap function calls in an arrow function when using
toThrow expect.any()andexpect.objectContaining()enable flexible assertions
Exercises
Exercise 1: Basics
Fill in the blanks in the following test.
test('string matchers', () => {
const message = 'Welcome to Jest testing!';
expect(message).___('Welcome'); // starts with "Welcome"
expect(message).___('testing'); // contains "testing"
expect(message).___(/Jest/); // matches regex
expect(message).___('Hello'); // does NOT contain "Hello"
});
Exercise 2: Intermediate
Write tests to verify the following API response. Verify that id is any number and createdAt is any string.
const response = {
id: 42,
name: 'Alice',
email: 'alice@example.com',
createdAt: '2025-01-01T00:00:00Z',
};
Challenge
Write tests for the validate method of the following Password class. Cover all error cases and the success case.
class Password {
constructor(value) {
this.value = value;
}
validate() {
if (this.value.length < 8) {
throw new Error('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(this.value)) {
throw new Error('Password must contain an uppercase letter');
}
if (!/[0-9]/.test(this.value)) {
throw new Error('Password must contain a number');
}
return true;
}
}
References
Next up: In Day 4, we'll learn about "Mocks, Stubs, and Spies." You'll master jest.fn(), jest.mock(), and jest.spyOn() to isolate dependencies and write focused unit tests!