0
# Testing and Mocking
1
2
Complete mocking system for testing HTTP interactions without real network calls. Undici provides a comprehensive mock framework that allows you to intercept requests, simulate responses, and verify API interactions in your tests.
3
4
## Capabilities
5
6
### Mock Agent
7
8
Central mock management system for controlling HTTP requests during testing.
9
10
```typescript { .api }
11
/**
12
* Mock agent for intercepting and simulating HTTP requests
13
* Provides complete control over network behavior in tests
14
*/
15
class MockAgent extends Dispatcher {
16
constructor(options?: MockAgent.Options);
17
18
/** Get mock dispatcher for specific origin */
19
get(origin: string): MockClient;
20
21
/** Close all mock dispatchers */
22
close(): Promise<void>;
23
24
/** Deactivate mocking (requests pass through) */
25
deactivate(): void;
26
27
/** Activate mocking (requests are intercepted) */
28
activate(): void;
29
30
/** Enable network connections for unmatched requests */
31
enableNetConnect(matcher?: string | RegExp | ((origin: string) => boolean)): void;
32
33
/** Disable all network connections */
34
disableNetConnect(): void;
35
36
/** Get history of all mock calls */
37
getCallHistory(): MockCallHistory[];
38
39
/** Clear call history */
40
clearCallHistory(): void;
41
42
/** Enable call history tracking */
43
enableCallHistory(): void;
44
45
/** Disable call history tracking */
46
disableCallHistory(): void;
47
48
/** Get list of pending interceptors */
49
pendingInterceptors(): PendingInterceptor[];
50
51
/** Assert no pending interceptors remain */
52
assertNoPendingInterceptors(options?: {
53
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
54
}): void;
55
}
56
57
interface MockAgent.Options {
58
/** Agent options passed to underlying agent */
59
agent?: Agent.Options;
60
61
/** Keep alive connections during testing */
62
keepAliveTimeout?: number;
63
64
/** Maximum keep alive timeout */
65
keepAliveMaxTimeout?: number;
66
}
67
68
interface PendingInterceptor {
69
origin: string;
70
method: string;
71
path: string;
72
data?: any;
73
persist: boolean;
74
times: number | null;
75
timesInvoked: number;
76
error: Error | null;
77
}
78
79
interface PendingInterceptorsFormatter {
80
(pendingInterceptors: readonly PendingInterceptor[]): string;
81
}
82
```
83
84
**Usage Examples:**
85
86
```typescript
87
import { MockAgent, setGlobalDispatcher } from "undici-types";
88
89
// Basic mock agent setup
90
const mockAgent = new MockAgent();
91
setGlobalDispatcher(mockAgent);
92
93
// Enable call history for verification
94
mockAgent.enableCallHistory();
95
96
// Disable real network connections
97
mockAgent.disableNetConnect();
98
99
// Allow connections to specific origins
100
mockAgent.enableNetConnect("https://allowed-external-api.com");
101
mockAgent.enableNetConnect(/^https:\/\/.*\.trusted\.com$/);
102
103
// Test cleanup
104
afterEach(() => {
105
mockAgent.clearCallHistory();
106
});
107
108
afterAll(async () => {
109
await mockAgent.close();
110
});
111
```
112
113
### Mock Client
114
115
Mock dispatcher for a specific origin with request interception capabilities.
116
117
```typescript { .api }
118
/**
119
* Mock client for specific origin
120
* Handles request interception and response simulation
121
*/
122
class MockClient extends Dispatcher {
123
/** Create interceptor for matching requests */
124
intercept(options: MockInterceptor.Options): MockInterceptor;
125
126
/** Close the mock client */
127
close(): Promise<void>;
128
129
/** Dispatch method (typically not called directly) */
130
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean;
131
}
132
133
interface MockInterceptor.Options {
134
/** Request path (string or regex) */
135
path: string | RegExp | ((path: string) => boolean);
136
137
/** HTTP method */
138
method?: string | RegExp;
139
140
/** Request headers matcher */
141
headers?: Record<string, string | RegExp | ((value: string) => boolean)>;
142
143
/** Request body matcher */
144
body?: string | RegExp | ((body: string) => boolean);
145
146
/** Query parameters matcher */
147
query?: Record<string, string | RegExp | ((value: string) => boolean)>;
148
}
149
150
/**
151
* Mock interceptor for configuring responses
152
*/
153
interface MockInterceptor {
154
/** Return successful response */
155
reply<T = any>(
156
status: number,
157
data?: T,
158
responseOptions?: MockInterceptor.ReplyOptions
159
): MockScope;
160
161
/** Return response with custom function */
162
reply<T = any>(
163
replyFunction: MockInterceptor.ReplyFunction<T>
164
): MockScope;
165
166
/** Return error response */
167
replyWithError(error: Error): MockScope;
168
169
/** Default reply for unmatched requests */
170
defaultReplyHeaders(headers: Record<string, string>): MockInterceptor;
171
172
/** Default reply trailers */
173
defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
174
}
175
176
interface MockInterceptor.ReplyOptions {
177
headers?: Record<string, string | string[]>;
178
trailers?: Record<string, string>;
179
}
180
181
interface MockInterceptor.ReplyFunction<T = any> {
182
(opts: {
183
path: string;
184
method: string;
185
body: any;
186
headers: Record<string, string>;
187
}): MockInterceptor.ReplyOptionsWithData<T>;
188
}
189
190
interface MockInterceptor.ReplyOptionsWithData<T = any> extends MockInterceptor.ReplyOptions {
191
statusCode: number;
192
data?: T;
193
}
194
195
/**
196
* Mock scope for controlling interceptor behavior
197
*/
198
interface MockScope {
199
/** Make interceptor persistent (reuse for multiple requests) */
200
persist(): MockScope;
201
202
/** Specify number of times interceptor should match */
203
times(times: number): MockScope;
204
205
/** Delay response by specified milliseconds */
206
delay(delay: number): MockScope;
207
}
208
```
209
210
**Usage Examples:**
211
212
```typescript
213
import { MockAgent, request } from "undici-types";
214
215
const mockAgent = new MockAgent();
216
const mockClient = mockAgent.get("https://api.example.com");
217
218
// Simple response mocking
219
mockClient
220
.intercept({ path: "/users", method: "GET" })
221
.reply(200, [
222
{ id: 1, name: "John Doe" },
223
{ id: 2, name: "Jane Smith" }
224
]);
225
226
// Test the mocked endpoint
227
const response = await request("https://api.example.com/users");
228
const users = await response.body.json();
229
console.log(users); // [{ id: 1, name: "John Doe" }, ...]
230
231
// Mock with headers
232
mockClient
233
.intercept({ path: "/protected", method: "GET" })
234
.reply(200, { data: "secret" }, {
235
headers: {
236
"Content-Type": "application/json",
237
"X-Custom-Header": "test-value"
238
}
239
});
240
241
// Mock with request matching
242
mockClient
243
.intercept({
244
path: "/users",
245
method: "POST",
246
headers: {
247
"content-type": "application/json"
248
},
249
body: (body) => {
250
const data = JSON.parse(body);
251
return data.name && data.email;
252
}
253
})
254
.reply(201, { id: 3, name: "New User" });
255
256
// Persistent interceptor (reused multiple times)
257
mockClient
258
.intercept({ path: "/ping", method: "GET" })
259
.reply(200, { status: "ok" })
260
.persist();
261
262
// Multiple requests to same endpoint
263
await request("https://api.example.com/ping"); // Works
264
await request("https://api.example.com/ping"); // Still works
265
266
// Limited use interceptor
267
mockClient
268
.intercept({ path: "/limited", method: "GET" })
269
.reply(200, { data: "limited" })
270
.times(2);
271
272
// Delayed response
273
mockClient
274
.intercept({ path: "/slow", method: "GET" })
275
.reply(200, { data: "delayed" })
276
.delay(1000);
277
```
278
279
### Mock Pool
280
281
Mock connection pool for testing pool-specific behavior.
282
283
```typescript { .api }
284
/**
285
* Mock pool for testing connection pool behavior
286
*/
287
class MockPool extends MockClient {
288
constructor(origin: string, options?: MockPool.Options);
289
}
290
291
interface MockPool.Options {
292
/** Agent options */
293
agent?: Agent.Options;
294
295
/** Mock-specific options */
296
connections?: number;
297
}
298
```
299
300
**Usage Examples:**
301
302
```typescript
303
import { MockPool } from "undici-types";
304
305
// Create mock pool directly
306
const mockPool = new MockPool("https://api.example.com", {
307
connections: 10
308
});
309
310
// Set up interceptors
311
mockPool
312
.intercept({ path: "/data", method: "GET" })
313
.reply(200, { items: [] });
314
315
// Use mock pool
316
const response = await mockPool.request({
317
path: "/data",
318
method: "GET"
319
});
320
```
321
322
### Advanced Mocking Patterns
323
324
Complex response simulation and dynamic behavior.
325
326
```typescript { .api }
327
/**
328
* Dynamic response function signature
329
*/
330
interface MockInterceptor.ReplyFunction<T = any> {
331
(opts: {
332
path: string;
333
method: string;
334
body: any;
335
headers: Record<string, string>;
336
query: Record<string, string>;
337
}): MockInterceptor.ReplyOptionsWithData<T> | Promise<MockInterceptor.ReplyOptionsWithData<T>>;
338
}
339
```
340
341
**Usage Examples:**
342
343
```typescript
344
import { MockAgent } from "undici-types";
345
346
const mockAgent = new MockAgent();
347
const mockClient = mockAgent.get("https://api.example.com");
348
349
// Dynamic response based on request
350
mockClient
351
.intercept({ path: /\/users\/(\d+)/, method: "GET" })
352
.reply((opts) => {
353
const userId = opts.path.match(/\/users\/(\d+)/)?.[1];
354
355
if (!userId) {
356
return { statusCode: 400, data: { error: "Invalid user ID" } };
357
}
358
359
return {
360
statusCode: 200,
361
data: { id: parseInt(userId), name: `User ${userId}` },
362
headers: { "X-User-ID": userId }
363
};
364
});
365
366
// Stateful mock with counter
367
let requestCount = 0;
368
mockClient
369
.intercept({ path: "/counter", method: "GET" })
370
.reply(() => {
371
requestCount++;
372
return {
373
statusCode: 200,
374
data: { count: requestCount },
375
headers: { "X-Request-Count": requestCount.toString() }
376
};
377
})
378
.persist();
379
380
// Mock with request body processing
381
mockClient
382
.intercept({ path: "/echo", method: "POST" })
383
.reply((opts) => {
384
let body;
385
try {
386
body = JSON.parse(opts.body);
387
} catch {
388
return { statusCode: 400, data: { error: "Invalid JSON" } };
389
}
390
391
return {
392
statusCode: 200,
393
data: {
394
received: body,
395
headers: opts.headers,
396
timestamp: new Date().toISOString()
397
}
398
};
399
});
400
401
// Conditional responses based on headers
402
mockClient
403
.intercept({ path: "/auth", method: "GET" })
404
.reply((opts) => {
405
const authHeader = opts.headers["authorization"];
406
407
if (!authHeader) {
408
return { statusCode: 401, data: { error: "Missing authorization" } };
409
}
410
411
if (authHeader === "Bearer valid-token") {
412
return { statusCode: 200, data: { user: "authenticated" } };
413
}
414
415
return { statusCode: 403, data: { error: "Invalid token" } };
416
});
417
```
418
419
### Call History and Verification
420
421
Track and verify mock interactions for test assertions.
422
423
```typescript { .api }
424
interface MockCallHistory {
425
origin: string;
426
method: string;
427
path: string;
428
headers: Record<string, string>;
429
body?: any;
430
}
431
432
interface MockCallHistoryLog extends MockCallHistory {
433
timestamp: number;
434
duration: number;
435
response: {
436
statusCode: number;
437
headers: Record<string, string>;
438
body?: any;
439
};
440
}
441
```
442
443
**Usage Examples:**
444
445
```typescript
446
import { MockAgent } from "undici-types";
447
448
const mockAgent = new MockAgent();
449
mockAgent.enableCallHistory();
450
451
const mockClient = mockAgent.get("https://api.example.com");
452
453
// Set up mocks
454
mockClient
455
.intercept({ path: "/users", method: "GET" })
456
.reply(200, []);
457
458
mockClient
459
.intercept({ path: "/users", method: "POST" })
460
.reply(201, { id: 1 });
461
462
// Make requests
463
await request("https://api.example.com/users", { method: "GET" });
464
await request("https://api.example.com/users", {
465
method: "POST",
466
body: JSON.stringify({ name: "John" }),
467
headers: { "content-type": "application/json" }
468
});
469
470
// Verify call history
471
const history = mockAgent.getCallHistory();
472
expect(history).toHaveLength(2);
473
474
// Check first call
475
expect(history[0]).toMatchObject({
476
origin: "https://api.example.com",
477
method: "GET",
478
path: "/users"
479
});
480
481
// Check second call
482
expect(history[1]).toMatchObject({
483
origin: "https://api.example.com",
484
method: "POST",
485
path: "/users",
486
headers: { "content-type": "application/json" }
487
});
488
489
// Verify specific request was made
490
const postCalls = history.filter(call =>
491
call.method === "POST" && call.path === "/users"
492
);
493
expect(postCalls).toHaveLength(1);
494
expect(JSON.parse(postCalls[0].body)).toEqual({ name: "John" });
495
```
496
497
### Error Simulation
498
499
Simulate network errors and failures for robust error handling testing.
500
501
```typescript { .api }
502
/**
503
* Mock interceptor error simulation
504
*/
505
interface MockInterceptor {
506
/** Simulate network or HTTP errors */
507
replyWithError(error: Error): MockScope;
508
}
509
```
510
511
**Usage Examples:**
512
513
```typescript
514
import {
515
MockAgent,
516
ConnectTimeoutError,
517
ResponseStatusCodeError,
518
request
519
} from "undici-types";
520
521
const mockAgent = new MockAgent();
522
const mockClient = mockAgent.get("https://api.example.com");
523
524
// Simulate connection timeout
525
mockClient
526
.intercept({ path: "/timeout", method: "GET" })
527
.replyWithError(new ConnectTimeoutError("Connection timed out"));
528
529
// Simulate server error
530
mockClient
531
.intercept({ path: "/server-error", method: "GET" })
532
.replyWithError(new ResponseStatusCodeError(
533
"Internal Server Error",
534
500,
535
{
536
"content-type": "application/json"
537
},
538
JSON.stringify({ error: "Internal server error" })
539
));
540
541
// Simulate network error
542
mockClient
543
.intercept({ path: "/network-error", method: "GET" })
544
.replyWithError(new Error("Network unreachable"));
545
546
// Test error handling
547
try {
548
await request("https://api.example.com/timeout");
549
} catch (error) {
550
expect(error).toBeInstanceOf(ConnectTimeoutError);
551
}
552
553
try {
554
await request("https://api.example.com/server-error");
555
} catch (error) {
556
expect(error).toBeInstanceOf(ResponseStatusCodeError);
557
expect((error as ResponseStatusCodeError).status).toBe(500);
558
}
559
560
// Intermittent errors
561
let callCount = 0;
562
mockClient
563
.intercept({ path: "/flaky", method: "GET" })
564
.reply(() => {
565
callCount++;
566
if (callCount <= 2) {
567
throw new Error("Service temporarily unavailable");
568
}
569
return { statusCode: 200, data: { success: true } };
570
})
571
.persist();
572
```
573
574
### Test Utilities
575
576
Helper functions for testing and assertion.
577
578
```typescript { .api }
579
/**
580
* Assert no pending interceptors remain after test
581
*/
582
MockAgent.prototype.assertNoPendingInterceptors(options?: {
583
pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
584
}): void;
585
586
/**
587
* Get pending interceptors that haven't been matched
588
*/
589
MockAgent.prototype.pendingInterceptors(): PendingInterceptor[];
590
```
591
592
**Usage Examples:**
593
594
```typescript
595
import { MockAgent } from "undici-types";
596
597
describe("API tests", () => {
598
let mockAgent: MockAgent;
599
600
beforeEach(() => {
601
mockAgent = new MockAgent();
602
setGlobalDispatcher(mockAgent);
603
mockAgent.disableNetConnect();
604
});
605
606
afterEach(() => {
607
// Ensure all mocks were used
608
mockAgent.assertNoPendingInterceptors({
609
pendingInterceptorsFormatter: (interceptors) => {
610
return interceptors
611
.map(i => `${i.method} ${i.path}`)
612
.join(', ');
613
}
614
});
615
616
mockAgent.clearCallHistory();
617
});
618
619
afterAll(async () => {
620
await mockAgent.close();
621
});
622
623
test("should use all interceptors", async () => {
624
const mockClient = mockAgent.get("https://api.example.com");
625
626
mockClient
627
.intercept({ path: "/users", method: "GET" })
628
.reply(200, []);
629
630
mockClient
631
.intercept({ path: "/posts", method: "GET" })
632
.reply(200, []);
633
634
// Make both requests
635
await request("https://api.example.com/users");
636
await request("https://api.example.com/posts");
637
638
// assertNoPendingInterceptors will pass in afterEach
639
});
640
641
test("will fail if interceptors unused", async () => {
642
const mockClient = mockAgent.get("https://api.example.com");
643
644
mockClient
645
.intercept({ path: "/users", method: "GET" })
646
.reply(200, []);
647
648
mockClient
649
.intercept({ path: "/posts", method: "GET" })
650
.reply(200, []); // This won't be used
651
652
// Only make one request
653
await request("https://api.example.com/users");
654
655
// assertNoPendingInterceptors will throw in afterEach
656
});
657
});
658
```