CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-undici

An HTTP/1.1 client, written from scratch for Node.js

Pending
Overview
Eval results
Files

mock-testing.mddocs/

Mock Testing Framework

Comprehensive testing utilities for mocking HTTP requests, recording interactions, and testing HTTP clients with detailed call history tracking.

Capabilities

MockAgent

Main mocking agent that intercepts HTTP requests and provides mock responses for testing.

/**
 * Main mocking agent for HTTP request interception
 */
class MockAgent extends Dispatcher {
  constructor(options?: MockAgent.Options);
  
  get(origin: string | RegExp | URL): MockPool;
  close(): Promise<void>;
  destroy(err?: Error): Promise<void>;
  
  activate(): void;
  deactivate(): void;
  enableNetConnect(host?: string | RegExp | ((host: string) => boolean)): void;
  disableNetConnect(): void;
  pendingInterceptors(): PendingInterceptor[];
  assertNoPendingInterceptors(options?: AssertNoPendingInterceptorsOptions): void;
}

interface MockAgent.Options {
  agent?: Agent;
  connections?: number;
}

interface AssertNoPendingInterceptorsOptions {
  pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}

type PendingInterceptorsFormatter = (pendingInterceptors: PendingInterceptor[]) => string;

Usage Examples:

import { MockAgent, setGlobalDispatcher } from 'undici';

// Create mock agent
const mockAgent = new MockAgent();

// Set as global dispatcher to intercept all requests
setGlobalDispatcher(mockAgent);

// Allow connections to specific hosts
mockAgent.enableNetConnect('api.github.com');

// Get mock pool for specific origin
const mockPool = mockAgent.get('https://api.example.com');

// Create mock interceptor
mockPool.intercept({
  path: '/users',
  method: 'GET'
}).reply(200, { users: [{ id: 1, name: 'Alice' }] });

// Make request (will be intercepted)
const response = await fetch('https://api.example.com/users');
const data = await response.json();
console.log(data); // { users: [{ id: 1, name: 'Alice' }] }

// Assert all interceptors were used
mockAgent.assertNoPendingInterceptors();

// Clean up
await mockAgent.close();

MockPool

Mock pool for intercepting requests to a specific origin with detailed interceptor management.

/**
 * Mock pool for specific origin request interception
 */
class MockPool extends Dispatcher {
  intercept(options: MockInterceptor.Options): MockInterceptor;
  close(): Promise<void>;
  destroy(err?: Error): Promise<void>;
}

interface MockInterceptor {
  reply(statusCode: number, responseBody?: any, responseHeaders?: Record<string, string>): MockScope;
  reply(callback: MockInterceptor.MockReplyCallback): MockScope;
  replyWithError(error: Error): MockScope;
  defaultReplyHeaders(headers: Record<string, string>): MockInterceptor;
  defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
  replyContentLength(): MockInterceptor;
}

interface MockInterceptor.Options {
  path: string | RegExp | ((path: string) => boolean);
  method?: string | RegExp;
  body?: string | Buffer | Uint8Array | RegExp | ((body: string) => boolean);
  headers?: Record<string, string | RegExp | ((headerValue: string) => boolean)>;
  query?: Record<string, any>;
}

type MockInterceptor.MockReplyCallback = (options: {
  path: string;
  origin: string;
  method: string;
  body: any;
  headers: Record<string, string>;
}) => {
  statusCode: number;
  data?: any;
  responseOptions?: {
    headers?: Record<string, string>;
    trailers?: Record<string, string>;
  };
};

interface MockScope {
  delay(waitInMs: number): MockScope;
  persist(): MockScope;
  times(repeatTimes: number): MockScope;
}

Usage Examples:

import { MockAgent, setGlobalDispatcher } from 'undici';

const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);

const mockPool = mockAgent.get('https://api.example.com');

// Basic mock with static response
mockPool.intercept({
  path: '/users/123',
  method: 'GET'
}).reply(200, { id: 123, name: 'Alice' });

// Mock with custom headers
mockPool.intercept({
  path: '/api/data',
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'authorization': /^Bearer .+/
  }
}).reply(201, { success: true }, {
  'location': '/api/data/456'
});

// Mock with body matching
mockPool.intercept({
  path: '/api/search',
  method: 'POST',
  body: (body) => {
    const data = JSON.parse(body);
    return data.query === 'test';
  }
}).reply(200, { results: ['item1', 'item2'] });

// Mock with query parameters
mockPool.intercept({
  path: '/api/users',
  method: 'GET',
  query: { limit: '10', offset: '0' }
}).reply(200, { users: [], total: 0 });

// Dynamic response with callback
mockPool.intercept({
  path: '/api/echo',
  method: 'POST'
}).reply(({ body, headers }) => {
  return {
    statusCode: 200,
    data: { echoed: body, receivedHeaders: headers }
  };
});

// Mock with scope options
mockPool.intercept({
  path: '/api/slow',
  method: 'GET'
}).reply(200, { data: 'slow response' })
  .delay(1000)  // 1 second delay
  .times(3)     // Only match 3 times
  .persist();   // Persist after times limit

// Mock error responses
mockPool.intercept({
  path: '/api/error',
  method: 'GET'
}).replyWithError(new Error('Network error'));

await mockAgent.close();

MockClient

Mock client for testing single connection scenarios.

/**
 * Mock client for single connection testing
 */
class MockClient extends Dispatcher {
  constructor(origin: string, options?: MockClient.Options);
  
  intercept(options: MockInterceptor.Options): MockInterceptor;
  close(): Promise<void>;
  destroy(err?: Error): Promise<void>;
}

interface MockClient.Options {
  agent?: Agent;
}

Usage Examples:

import { MockClient } from 'undici';

// Create mock client for specific origin
const mockClient = new MockClient('https://api.example.com');

// Setup interceptors
mockClient.intercept({
  path: '/health',
  method: 'GET'
}).reply(200, { status: 'ok' });

// Use mock client directly
const response = await mockClient.request({
  path: '/health',
  method: 'GET'
});

const data = await response.body.json();
console.log(data); // { status: 'ok' }

await mockClient.close();

SnapshotAgent

Recording agent for creating HTTP interaction snapshots that can be replayed in tests.

/**
 * Recording agent for HTTP interaction snapshots
 */
class SnapshotAgent extends Dispatcher {
  constructor(origin: string, dispatcher: Dispatcher);
  
  record(options?: SnapshotAgent.RecordOptions): void;
  replay(options?: SnapshotAgent.ReplayOptions): void;
  getRecording(): SnapshotRecording[];
  setRecording(recording: SnapshotRecording[]): void;
  
  close(): Promise<void>;
  destroy(err?: Error): Promise<void>;
}

interface SnapshotAgent.RecordOptions {
  filter?: (request: SnapshotRequest) => boolean;
}

interface SnapshotAgent.ReplayOptions {
  strict?: boolean;
}

interface SnapshotRecording {
  request: SnapshotRequest;
  response: SnapshotResponse;
  timestamp: number;
}

interface SnapshotRequest {
  method: string;
  path: string;
  headers: Record<string, string>;
  body?: string;
}

interface SnapshotResponse {
  statusCode: number;
  headers: Record<string, string>;
  body: string;
}

Usage Examples:

import { SnapshotAgent, Agent } from 'undici';
import { writeFileSync, readFileSync } from 'fs';

const realAgent = new Agent();
const snapshotAgent = new SnapshotAgent('https://api.example.com', realAgent);

// Record real HTTP interactions
snapshotAgent.record();

// Make real requests (these will be recorded)
await snapshotAgent.request({ path: '/users' });
await snapshotAgent.request({ path: '/posts' });

// Save recording to file
const recording = snapshotAgent.getRecording();
writeFileSync('test-recording.json', JSON.stringify(recording, null, 2));

// Later, in tests: load and replay recording
const savedRecording = JSON.parse(readFileSync('test-recording.json', 'utf8'));
snapshotAgent.setRecording(savedRecording);
snapshotAgent.replay();

// Requests will now return recorded responses
const response = await snapshotAgent.request({ path: '/users' });
console.log('Replayed response:', await response.body.json());

await snapshotAgent.close();

MockCallHistory

Track and verify mock call history for detailed test assertions.

/**
 * Track mock call history for test verification
 */
class MockCallHistory {
  constructor();
  
  log(call: MockCallHistoryLog): void;
  getCalls(filter?: MockCallHistoryFilter): MockCallHistoryLog[];
  getCallCount(filter?: MockCallHistoryFilter): number;
  hasBeenCalledWith(expectedCall: Partial<MockCallHistoryLog>): boolean;
  clear(): void;
}

interface MockCallHistoryLog {
  method: string;
  path: string;
  headers: Record<string, string>;
  body?: any;
  timestamp: number;
  origin: string;
}

type MockCallHistoryFilter = (call: MockCallHistoryLog) => boolean;

Usage Examples:

import { MockAgent, MockCallHistory, setGlobalDispatcher } from 'undici';

const mockAgent = new MockAgent();
const callHistory = new MockCallHistory();
setGlobalDispatcher(mockAgent);

// Setup mock with call logging
const mockPool = mockAgent.get('https://api.example.com');
mockPool.intercept({
  path: '/api/users',
  method: 'POST'
}).reply(201, { id: 123 });

// Intercept and log all calls (this would be done internally by undici)
const originalRequest = mockPool.request;
mockPool.request = function(options) {
  callHistory.log({
    method: options.method || 'GET',
    path: options.path,
    headers: options.headers || {},
    body: options.body,
    timestamp: Date.now(),
    origin: 'https://api.example.com'
  });
  return originalRequest.call(this, options);
};

// Make requests
await fetch('https://api.example.com/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice' })
});

// Verify call history
console.log(callHistory.getCallCount()); // 1

const calls = callHistory.getCalls();
console.log(calls[0].method); // 'POST'
console.log(calls[0].path);   // '/api/users'

// Check specific call
const wasCalledWithCorrectData = callHistory.hasBeenCalledWith({
  method: 'POST',
  path: '/api/users'
});
console.log(wasCalledWithCorrectData); // true

// Filter calls
const postCalls = callHistory.getCalls(call => call.method === 'POST');
console.log(postCalls.length); // 1

await mockAgent.close();

Mock Errors

Specialized error types for mock testing scenarios.

/**
 * Mock-specific error types
 */
const mockErrors: {
  MockNotMatchedError: typeof MockNotMatchedError;
  MockInterceptorMismatchError: typeof MockInterceptorMismatchError;
};

class MockNotMatchedError extends Error {
  constructor(message: string);
}

class MockInterceptorMismatchError extends Error {
  constructor(message: string);
}

Usage Examples:

import { MockAgent, mockErrors, setGlobalDispatcher } from 'undici';

const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);

// Disable real network connections
mockAgent.disableNetConnect();

try {
  // This will throw since no mock is defined
  await fetch('https://api.example.com/unmocked');
} catch (error) {
  if (error instanceof mockErrors.MockNotMatchedError) {
    console.log('No mock interceptor matched the request');
  }
}

// Setup specific error scenarios
const mockPool = mockAgent.get('https://api.example.com');
mockPool.intercept({
  path: '/api/error-test',
  method: 'GET'
}).replyWithError(new mockErrors.MockInterceptorMismatchError('Intentional test error'));

await mockAgent.close();

Complete Testing Workflow

import { MockAgent, setGlobalDispatcher, MockCallHistory } from 'undici';
import { test, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert';

let mockAgent;
let callHistory;

beforeEach(() => {
  mockAgent = new MockAgent();
  callHistory = new MockCallHistory();
  setGlobalDispatcher(mockAgent);
  
  // Disable real network connections in tests
  mockAgent.disableNetConnect();
});

afterEach(async () => {
  await mockAgent.close();
});

test('user service integration', async () => {
  const mockPool = mockAgent.get('https://api.example.com');
  
  // Mock user creation
  mockPool.intercept({
    path: '/users',
    method: 'POST',
    body: (body) => {
      const data = JSON.parse(body);
      return data.name && data.email;
    }
  }).reply(201, { id: 123, name: 'Alice', email: 'alice@example.com' });
  
  // Mock user retrieval
  mockPool.intercept({
    path: '/users/123',
    method: 'GET'
  }).reply(200, { id: 123, name: 'Alice', email: 'alice@example.com' });
  
  // Test the actual service
  const createResponse = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ name: 'Alice', email: 'alice@example.com' })
  });
  
  assert.strictEqual(createResponse.status, 201);
  
  const getResponse = await fetch('https://api.example.com/users/123');
  const userData = await getResponse.json();
  
  assert.strictEqual(userData.name, 'Alice');
  assert.strictEqual(userData.email, 'alice@example.com');
  
  // Verify all mocks were called
  mockAgent.assertNoPendingInterceptors();
});

Install with Tessl CLI

npx tessl i tessl/npm-undici

docs

caching.md

connection-management.md

cookies.md

core-http.md

errors.md

global-config.md

headers-body.md

index.md

interceptors.md

mock-testing.md

web-standards.md

tile.json