0
# Configuration and Extension
1
2
Methods for extending Jest Matchers with custom matchers and configuring test behavior. These utilities allow for customization of the testing framework and assertion counting.
3
4
## Capabilities
5
6
### Custom Matcher Extension
7
8
#### expect.extend
9
10
Adds custom matchers to the expect function, allowing you to create domain-specific assertions.
11
12
```javascript { .api }
13
/**
14
* Adds custom matchers to the expect function
15
* @param matchers - Object containing custom matcher functions
16
*/
17
expect.extend(matchers: MatchersObject): void;
18
19
interface MatchersObject {
20
[matcherName: string]: RawMatcherFn;
21
}
22
23
interface RawMatcherFn {
24
(this: MatcherState, received: any, ...args: any[]): ExpectationResult;
25
}
26
27
interface ExpectationResult {
28
pass: boolean;
29
message?: string | (() => string);
30
}
31
32
interface MatcherState {
33
isNot: boolean;
34
utils: MatcherUtils;
35
equals: (a: any, b: any) => boolean;
36
dontThrow: () => void;
37
}
38
```
39
40
**Usage Examples:**
41
42
```javascript
43
// Define custom matchers
44
expect.extend({
45
toBeEven(received) {
46
const pass = received % 2 === 0;
47
return {
48
pass,
49
message: () =>
50
`expected ${received} ${pass ? 'not ' : ''}to be even`
51
};
52
},
53
54
toBeWithinRange(received, floor, ceiling) {
55
const pass = received >= floor && received <= ceiling;
56
return {
57
pass,
58
message: () =>
59
`expected ${received} ${pass ? 'not ' : ''}to be within range ${floor} - ${ceiling}`
60
};
61
},
62
63
toHaveValidEmail(received) {
64
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
65
const pass = typeof received === 'string' && emailRegex.test(received);
66
67
return {
68
pass,
69
message: () =>
70
`expected ${received} ${pass ? 'not ' : ''}to be a valid email address`
71
};
72
}
73
});
74
75
// Use custom matchers
76
expect(4).toBeEven();
77
expect(3).not.toBeEven();
78
79
expect(5).toBeWithinRange(1, 10);
80
expect(15).not.toBeWithinRange(1, 10);
81
82
expect('user@example.com').toHaveValidEmail();
83
expect('invalid-email').not.toHaveValidEmail();
84
```
85
86
**Advanced Custom Matcher Examples:**
87
88
```javascript
89
expect.extend({
90
// Async matcher
91
async toEventuallyEqual(received, expected, timeout = 1000) {
92
const startTime = Date.now();
93
94
while (Date.now() - startTime < timeout) {
95
if (this.equals(received(), expected)) {
96
return {
97
pass: true,
98
message: () => `expected function not to eventually return ${expected}`
99
};
100
}
101
await new Promise(resolve => setTimeout(resolve, 10));
102
}
103
104
return {
105
pass: false,
106
message: () => `expected function to eventually return ${expected} within ${timeout}ms`
107
};
108
},
109
110
// Matcher with detailed diff
111
toMatchUserSchema(received) {
112
const requiredFields = ['id', 'name', 'email'];
113
const missingFields = requiredFields.filter(field => !(field in received));
114
const pass = missingFields.length === 0;
115
116
return {
117
pass,
118
message: () => {
119
if (pass) {
120
return `expected user object not to match schema`;
121
} else {
122
return `expected user object to match schema, missing fields: ${missingFields.join(', ')}`;
123
}
124
}
125
};
126
},
127
128
// Matcher using this.utils for formatting
129
toHaveProperty(received, property, expectedValue) {
130
const hasProperty = Object.prototype.hasOwnProperty.call(received, property);
131
const actualValue = received[property];
132
133
if (arguments.length === 2) {
134
// Only check property existence
135
return {
136
pass: hasProperty,
137
message: () =>
138
`expected object ${hasProperty ? 'not ' : ''}to have property '${property}'`
139
};
140
} else {
141
// Check property existence and value
142
const pass = hasProperty && this.equals(actualValue, expectedValue);
143
return {
144
pass,
145
message: () => {
146
if (!hasProperty) {
147
return `expected object to have property '${property}'`;
148
} else if (!this.equals(actualValue, expectedValue)) {
149
return `expected property '${property}' to be ${this.utils.printExpected(expectedValue)}, received ${this.utils.printReceived(actualValue)}`;
150
} else {
151
return `expected property '${property}' not to be ${this.utils.printExpected(expectedValue)}`;
152
}
153
}
154
};
155
}
156
}
157
});
158
159
// Usage
160
await expect(() => getCurrentValue()).toEventuallyEqual(42, 2000);
161
162
expect({id: 1, name: 'John', email: 'john@example.com'}).toMatchUserSchema();
163
164
expect(user).toHaveProperty('name', 'John');
165
```
166
167
### Assertion Counting
168
169
#### expect.assertions
170
171
Sets the expected number of assertions in a test. Useful for ensuring async code paths are tested.
172
173
```javascript { .api }
174
/**
175
* Sets expected number of assertions in a test
176
* @param expected - Expected number of assertions to be called
177
*/
178
expect.assertions(expected: number): void;
179
```
180
181
**Usage Examples:**
182
183
```javascript
184
// Ensure async callbacks are called
185
test('async test with assertion counting', async () => {
186
expect.assertions(2);
187
188
const callback = jest.fn((result) => {
189
expect(result).toBeDefined();
190
expect(result.success).toBe(true);
191
});
192
193
await processDataAsync(data, callback);
194
});
195
196
// Testing conditional code paths
197
test('conditional assertions', () => {
198
expect.assertions(1);
199
200
const value = getValue();
201
202
if (value > 0) {
203
expect(value).toBeGreaterThan(0);
204
} else {
205
expect(value).toBeLessThanOrEqual(0);
206
}
207
});
208
209
// Testing promise rejections
210
test('promise rejection', async () => {
211
expect.assertions(1);
212
213
try {
214
await riskyOperation();
215
} catch (error) {
216
expect(error.message).toContain('operation failed');
217
}
218
});
219
```
220
221
#### expect.hasAssertions
222
223
Verifies that at least one assertion is called during the test. More flexible than `expect.assertions()`.
224
225
```javascript { .api }
226
/**
227
* Verifies at least one assertion is called
228
*/
229
expect.hasAssertions(): void;
230
```
231
232
**Usage Examples:**
233
234
```javascript
235
// Ensure some assertion is made in complex async flow
236
test('complex async flow', async () => {
237
expect.hasAssertions();
238
239
const results = await Promise.all([
240
processItem(item1),
241
processItem(item2),
242
processItem(item3)
243
]);
244
245
results.forEach(result => {
246
if (result.shouldValidate) {
247
expect(result.data).toBeDefined();
248
}
249
});
250
});
251
252
// Testing event handlers
253
test('event handler', () => {
254
expect.hasAssertions();
255
256
const handler = (event) => {
257
if (event.type === 'error') {
258
expect(event.message).toBeDefined();
259
} else if (event.type === 'success') {
260
expect(event.data).toBeDefined();
261
}
262
};
263
264
emitter.emit('success', {data: 'test'});
265
});
266
```
267
268
### State Management
269
270
#### expect.getState
271
272
Gets the current global matcher state, including assertion counts and custom state.
273
274
```javascript { .api }
275
/**
276
* Gets current global matcher state
277
* @returns Current matcher state object
278
*/
279
expect.getState(): Object;
280
```
281
282
**Usage Examples:**
283
284
```javascript
285
// Check current assertion count
286
const state = expect.getState();
287
console.log(`Assertions so far: ${state.assertionCalls}`);
288
289
// Access custom state in matchers
290
expect.extend({
291
toTrackCalls(received) {
292
const state = expect.getState();
293
console.log(`This is assertion #${state.assertionCalls + 1}`);
294
295
return {
296
pass: true,
297
message: () => 'tracking call'
298
};
299
}
300
});
301
```
302
303
#### expect.setState
304
305
Sets or updates the global matcher state. Useful for sharing data between custom matchers.
306
307
```javascript { .api }
308
/**
309
* Sets global matcher state
310
* @param state - State object to merge with current state
311
*/
312
expect.setState(state: Object): void;
313
```
314
315
**Usage Examples:**
316
317
```javascript
318
// Set up shared state for custom matchers
319
expect.setState({
320
testStartTime: Date.now(),
321
customConfig: {
322
strictMode: true,
323
debugMode: false
324
}
325
});
326
327
// Custom matcher using shared state
328
expect.extend({
329
toCompleteWithinTime(received, maxDuration) {
330
const { testStartTime } = expect.getState();
331
const actualDuration = Date.now() - testStartTime;
332
const pass = actualDuration <= maxDuration;
333
334
return {
335
pass,
336
message: () =>
337
`expected test to complete within ${maxDuration}ms, took ${actualDuration}ms`
338
};
339
},
340
341
toRespectStrictMode(received) {
342
const { customConfig } = expect.getState();
343
344
if (!customConfig?.strictMode) {
345
return { pass: true, message: () => 'strict mode disabled' };
346
}
347
348
// Perform strict validation
349
const pass = validateStrict(received);
350
return {
351
pass,
352
message: () => `expected ${received} to pass strict validation`
353
};
354
}
355
});
356
357
// Usage
358
expect(result).toCompleteWithinTime(1000);
359
expect(data).toRespectStrictMode();
360
```
361
362
### Snapshot Serializer Support
363
364
#### expect.addSnapshotSerializer
365
366
No-op function for compatibility with Jest's snapshot serializer system.
367
368
```javascript { .api }
369
/**
370
* No-op function for snapshot serializer compatibility
371
*/
372
expect.addSnapshotSerializer(): void;
373
```
374
375
**Usage Examples:**
376
377
```javascript
378
// This is a compatibility function - it does nothing in jest-matchers
379
expect.addSnapshotSerializer();
380
381
// In full Jest environment, you would use it like:
382
// expect.addSnapshotSerializer({
383
// serialize: (val) => val.toString(),
384
// test: (val) => val instanceof MyClass
385
// });
386
```
387
388
## Advanced Configuration Patterns
389
390
### Setting Up Custom Matcher Suites
391
392
```javascript
393
// Create reusable matcher suites
394
const customMatchers = {
395
// Date matchers
396
toBeToday(received) {
397
const today = new Date().toDateString();
398
const receivedDate = new Date(received).toDateString();
399
const pass = today === receivedDate;
400
401
return {
402
pass,
403
message: () => `expected ${received} ${pass ? 'not ' : ''}to be today`
404
};
405
},
406
407
// HTTP status matchers
408
toBeSuccessStatus(received) {
409
const pass = received >= 200 && received < 300;
410
return {
411
pass,
412
message: () => `expected status ${received} ${pass ? 'not ' : ''}to be successful (2xx)`
413
};
414
},
415
416
// Object structure matchers
417
toHaveRequiredFields(received, requiredFields) {
418
const missingFields = requiredFields.filter(field => !(field in received));
419
const pass = missingFields.length === 0;
420
421
return {
422
pass,
423
message: () => {
424
if (pass) {
425
return `expected object not to have all required fields`;
426
} else {
427
return `expected object to have required fields: ${missingFields.join(', ')} missing`;
428
}
429
}
430
};
431
}
432
};
433
434
// Apply all custom matchers
435
expect.extend(customMatchers);
436
437
// Usage
438
expect(new Date()).toBeToday();
439
expect(200).toBeSuccessStatus();
440
expect(user).toHaveRequiredFields(['id', 'name', 'email']);
441
```
442
443
### Global Test Configuration
444
445
```javascript
446
// Set up global test state
447
beforeEach(() => {
448
expect.setState({
449
testStartTime: Date.now(),
450
testId: Math.random().toString(36),
451
config: {
452
timeout: 5000,
453
retries: 3
454
}
455
});
456
});
457
458
// Custom matchers that use global config
459
expect.extend({
460
toCompleteWithinTimeout(received) {
461
const { config, testStartTime } = expect.getState();
462
const duration = Date.now() - testStartTime;
463
const pass = duration <= config.timeout;
464
465
return {
466
pass,
467
message: () =>
468
`expected operation to complete within ${config.timeout}ms, took ${duration}ms`
469
};
470
}
471
});
472
```