or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

connection-management.mdcookies.mderror-handling.mdhttp-api.mdhttp-clients.mdindex.mdinterceptors.mdtesting-mocking.mdutilities.mdweb-standards.md

testing-mocking.mddocs/

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

```