CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-undici-types

A stand-alone types package for Undici HTTP client library

Pending
Overview
Eval results
Files

testing-mocking.mddocs/

Testing and Mocking

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.

Capabilities

Mock Agent

Central mock management system for controlling HTTP requests during testing.

/**
 * Mock agent for intercepting and simulating HTTP requests
 * Provides complete control over network behavior in tests
 */
class MockAgent extends Dispatcher {
  constructor(options?: MockAgent.Options);
  
  /** Get mock dispatcher for specific origin */
  get(origin: string): MockClient;
  
  /** Close all mock dispatchers */
  close(): Promise<void>;
  
  /** Deactivate mocking (requests pass through) */
  deactivate(): void;
  
  /** Activate mocking (requests are intercepted) */
  activate(): void;
  
  /** Enable network connections for unmatched requests */
  enableNetConnect(matcher?: string | RegExp | ((origin: string) => boolean)): void;
  
  /** Disable all network connections */
  disableNetConnect(): void;
  
  /** Get history of all mock calls */
  getCallHistory(): MockCallHistory[];
  
  /** Clear call history */
  clearCallHistory(): void;
  
  /** Enable call history tracking */
  enableCallHistory(): void;
  
  /** Disable call history tracking */
  disableCallHistory(): void;
  
  /** Get list of pending interceptors */
  pendingInterceptors(): PendingInterceptor[];
  
  /** Assert no pending interceptors remain */
  assertNoPendingInterceptors(options?: {
    pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
  }): void;
}

interface MockAgent.Options {
  /** Agent options passed to underlying agent */
  agent?: Agent.Options;
  
  /** Keep alive connections during testing */
  keepAliveTimeout?: number;
  
  /** Maximum keep alive timeout */
  keepAliveMaxTimeout?: number;
}

interface PendingInterceptor {
  origin: string;
  method: string;
  path: string;
  data?: any;
  persist: boolean;
  times: number | null;
  timesInvoked: number;
  error: Error | null;
}

interface PendingInterceptorsFormatter {
  (pendingInterceptors: readonly PendingInterceptor[]): string;
}

Usage Examples:

import { MockAgent, setGlobalDispatcher } from "undici-types";

// Basic mock agent setup
const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);

// Enable call history for verification
mockAgent.enableCallHistory();

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

// Allow connections to specific origins
mockAgent.enableNetConnect("https://allowed-external-api.com");
mockAgent.enableNetConnect(/^https:\/\/.*\.trusted\.com$/);

// Test cleanup
afterEach(() => {
  mockAgent.clearCallHistory();
});

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

Mock Client

Mock dispatcher for a specific origin with request interception capabilities.

/**
 * Mock client for specific origin
 * Handles request interception and response simulation
 */
class MockClient extends Dispatcher {
  /** Create interceptor for matching requests */
  intercept(options: MockInterceptor.Options): MockInterceptor;
  
  /** Close the mock client */
  close(): Promise<void>;
  
  /** Dispatch method (typically not called directly) */
  dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean;
}

interface MockInterceptor.Options {
  /** Request path (string or regex) */
  path: string | RegExp | ((path: string) => boolean);
  
  /** HTTP method */
  method?: string | RegExp;
  
  /** Request headers matcher */
  headers?: Record<string, string | RegExp | ((value: string) => boolean)>;
  
  /** Request body matcher */
  body?: string | RegExp | ((body: string) => boolean);
  
  /** Query parameters matcher */
  query?: Record<string, string | RegExp | ((value: string) => boolean)>;
}

/**
 * Mock interceptor for configuring responses
 */
interface MockInterceptor {
  /** Return successful response */
  reply<T = any>(
    status: number,
    data?: T,
    responseOptions?: MockInterceptor.ReplyOptions
  ): MockScope;
  
  /** Return response with custom function */
  reply<T = any>(
    replyFunction: MockInterceptor.ReplyFunction<T>
  ): MockScope;
  
  /** Return error response */
  replyWithError(error: Error): MockScope;
  
  /** Default reply for unmatched requests */
  defaultReplyHeaders(headers: Record<string, string>): MockInterceptor;
  
  /** Default reply trailers */
  defaultReplyTrailers(trailers: Record<string, string>): MockInterceptor;
}

interface MockInterceptor.ReplyOptions {
  headers?: Record<string, string | string[]>;
  trailers?: Record<string, string>;
}

interface MockInterceptor.ReplyFunction<T = any> {
  (opts: {
    path: string;
    method: string;
    body: any;
    headers: Record<string, string>;
  }): MockInterceptor.ReplyOptionsWithData<T>;
}

interface MockInterceptor.ReplyOptionsWithData<T = any> extends MockInterceptor.ReplyOptions {
  statusCode: number;
  data?: T;
}

/**
 * Mock scope for controlling interceptor behavior
 */
interface MockScope {
  /** Make interceptor persistent (reuse for multiple requests) */
  persist(): MockScope;
  
  /** Specify number of times interceptor should match */
  times(times: number): MockScope;
  
  /** Delay response by specified milliseconds */
  delay(delay: number): MockScope;
}

Usage Examples:

import { MockAgent, request } from "undici-types";

const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");

// Simple response mocking
mockClient
  .intercept({ path: "/users", method: "GET" })
  .reply(200, [
    { id: 1, name: "John Doe" },
    { id: 2, name: "Jane Smith" }
  ]);

// Test the mocked endpoint
const response = await request("https://api.example.com/users");
const users = await response.body.json();
console.log(users); // [{ id: 1, name: "John Doe" }, ...]

// Mock with headers
mockClient
  .intercept({ path: "/protected", method: "GET" })
  .reply(200, { data: "secret" }, {
    headers: {
      "Content-Type": "application/json",
      "X-Custom-Header": "test-value"
    }
  });

// Mock with request matching
mockClient
  .intercept({
    path: "/users",
    method: "POST",
    headers: {
      "content-type": "application/json"
    },
    body: (body) => {
      const data = JSON.parse(body);
      return data.name && data.email;
    }
  })
  .reply(201, { id: 3, name: "New User" });

// Persistent interceptor (reused multiple times)
mockClient
  .intercept({ path: "/ping", method: "GET" })
  .reply(200, { status: "ok" })
  .persist();

// Multiple requests to same endpoint
await request("https://api.example.com/ping"); // Works
await request("https://api.example.com/ping"); // Still works

// Limited use interceptor
mockClient
  .intercept({ path: "/limited", method: "GET" })
  .reply(200, { data: "limited" })
  .times(2);

// Delayed response
mockClient
  .intercept({ path: "/slow", method: "GET" })
  .reply(200, { data: "delayed" })
  .delay(1000);

Mock Pool

Mock connection pool for testing pool-specific behavior.

/**
 * Mock pool for testing connection pool behavior
 */
class MockPool extends MockClient {
  constructor(origin: string, options?: MockPool.Options);
}

interface MockPool.Options {
  /** Agent options */
  agent?: Agent.Options;
  
  /** Mock-specific options */
  connections?: number;
}

Usage Examples:

import { MockPool } from "undici-types";

// Create mock pool directly
const mockPool = new MockPool("https://api.example.com", {
  connections: 10
});

// Set up interceptors
mockPool
  .intercept({ path: "/data", method: "GET" })
  .reply(200, { items: [] });

// Use mock pool
const response = await mockPool.request({
  path: "/data",
  method: "GET"
});

Advanced Mocking Patterns

Complex response simulation and dynamic behavior.

/**
 * Dynamic response function signature
 */
interface MockInterceptor.ReplyFunction<T = any> {
  (opts: {
    path: string;
    method: string;
    body: any;
    headers: Record<string, string>;
    query: Record<string, string>;
  }): MockInterceptor.ReplyOptionsWithData<T> | Promise<MockInterceptor.ReplyOptionsWithData<T>>;
}

Usage Examples:

import { MockAgent } from "undici-types";

const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");

// Dynamic response based on request
mockClient
  .intercept({ path: /\/users\/(\d+)/, method: "GET" })
  .reply((opts) => {
    const userId = opts.path.match(/\/users\/(\d+)/)?.[1];
    
    if (!userId) {
      return { statusCode: 400, data: { error: "Invalid user ID" } };
    }
    
    return {
      statusCode: 200,
      data: { id: parseInt(userId), name: `User ${userId}` },
      headers: { "X-User-ID": userId }
    };
  });

// Stateful mock with counter
let requestCount = 0;
mockClient
  .intercept({ path: "/counter", method: "GET" })
  .reply(() => {
    requestCount++;
    return {
      statusCode: 200,
      data: { count: requestCount },
      headers: { "X-Request-Count": requestCount.toString() }
    };
  })
  .persist();

// Mock with request body processing
mockClient
  .intercept({ path: "/echo", method: "POST" })
  .reply((opts) => {
    let body;
    try {
      body = JSON.parse(opts.body);
    } catch {
      return { statusCode: 400, data: { error: "Invalid JSON" } };
    }
    
    return {
      statusCode: 200,
      data: {
        received: body,
        headers: opts.headers,
        timestamp: new Date().toISOString()
      }
    };
  });

// Conditional responses based on headers
mockClient
  .intercept({ path: "/auth", method: "GET" })
  .reply((opts) => {
    const authHeader = opts.headers["authorization"];
    
    if (!authHeader) {
      return { statusCode: 401, data: { error: "Missing authorization" } };
    }
    
    if (authHeader === "Bearer valid-token") {
      return { statusCode: 200, data: { user: "authenticated" } };
    }
    
    return { statusCode: 403, data: { error: "Invalid token" } };
  });

Call History and Verification

Track and verify mock interactions for test assertions.

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

interface MockCallHistoryLog extends MockCallHistory {
  timestamp: number;
  duration: number;
  response: {
    statusCode: number;
    headers: Record<string, string>;
    body?: any;
  };
}

Usage Examples:

import { MockAgent } from "undici-types";

const mockAgent = new MockAgent();
mockAgent.enableCallHistory();

const mockClient = mockAgent.get("https://api.example.com");

// Set up mocks
mockClient
  .intercept({ path: "/users", method: "GET" })
  .reply(200, []);

mockClient
  .intercept({ path: "/users", method: "POST" })
  .reply(201, { id: 1 });

// Make requests
await request("https://api.example.com/users", { method: "GET" });
await request("https://api.example.com/users", { 
  method: "POST",
  body: JSON.stringify({ name: "John" }),
  headers: { "content-type": "application/json" }
});

// Verify call history
const history = mockAgent.getCallHistory();
expect(history).toHaveLength(2);

// Check first call
expect(history[0]).toMatchObject({
  origin: "https://api.example.com",
  method: "GET",
  path: "/users"
});

// Check second call
expect(history[1]).toMatchObject({
  origin: "https://api.example.com",
  method: "POST",
  path: "/users",
  headers: { "content-type": "application/json" }
});

// Verify specific request was made
const postCalls = history.filter(call => 
  call.method === "POST" && call.path === "/users"
);
expect(postCalls).toHaveLength(1);
expect(JSON.parse(postCalls[0].body)).toEqual({ name: "John" });

Error Simulation

Simulate network errors and failures for robust error handling testing.

/**
 * Mock interceptor error simulation
 */
interface MockInterceptor {
  /** Simulate network or HTTP errors */
  replyWithError(error: Error): MockScope;
}

Usage Examples:

import { 
  MockAgent, 
  ConnectTimeoutError,
  ResponseStatusCodeError,
  request 
} from "undici-types";

const mockAgent = new MockAgent();
const mockClient = mockAgent.get("https://api.example.com");

// Simulate connection timeout
mockClient
  .intercept({ path: "/timeout", method: "GET" })
  .replyWithError(new ConnectTimeoutError("Connection timed out"));

// Simulate server error
mockClient
  .intercept({ path: "/server-error", method: "GET" })
  .replyWithError(new ResponseStatusCodeError(
    "Internal Server Error",
    500,
    {
      "content-type": "application/json"
    },
    JSON.stringify({ error: "Internal server error" })
  ));

// Simulate network error
mockClient
  .intercept({ path: "/network-error", method: "GET" })
  .replyWithError(new Error("Network unreachable"));

// Test error handling
try {
  await request("https://api.example.com/timeout");
} catch (error) {
  expect(error).toBeInstanceOf(ConnectTimeoutError);
}

try {
  await request("https://api.example.com/server-error");
} catch (error) {
  expect(error).toBeInstanceOf(ResponseStatusCodeError);
  expect((error as ResponseStatusCodeError).status).toBe(500);
}

// Intermittent errors
let callCount = 0;
mockClient
  .intercept({ path: "/flaky", method: "GET" })
  .reply(() => {
    callCount++;
    if (callCount <= 2) {
      throw new Error("Service temporarily unavailable");
    }
    return { statusCode: 200, data: { success: true } };
  })
  .persist();

Test Utilities

Helper functions for testing and assertion.

/**
 * Assert no pending interceptors remain after test
 */
MockAgent.prototype.assertNoPendingInterceptors(options?: {
  pendingInterceptorsFormatter?: PendingInterceptorsFormatter;
}): void;

/**
 * Get pending interceptors that haven't been matched
 */
MockAgent.prototype.pendingInterceptors(): PendingInterceptor[];

Usage Examples:

import { MockAgent } from "undici-types";

describe("API tests", () => {
  let mockAgent: MockAgent;
  
  beforeEach(() => {
    mockAgent = new MockAgent();
    setGlobalDispatcher(mockAgent);
    mockAgent.disableNetConnect();
  });
  
  afterEach(() => {
    // Ensure all mocks were used
    mockAgent.assertNoPendingInterceptors({
      pendingInterceptorsFormatter: (interceptors) => {
        return interceptors
          .map(i => `${i.method} ${i.path}`)
          .join(', ');
      }
    });
    
    mockAgent.clearCallHistory();
  });
  
  afterAll(async () => {
    await mockAgent.close();
  });
  
  test("should use all interceptors", async () => {
    const mockClient = mockAgent.get("https://api.example.com");
    
    mockClient
      .intercept({ path: "/users", method: "GET" })
      .reply(200, []);
    
    mockClient
      .intercept({ path: "/posts", method: "GET" })
      .reply(200, []);
    
    // Make both requests
    await request("https://api.example.com/users");
    await request("https://api.example.com/posts");
    
    // assertNoPendingInterceptors will pass in afterEach
  });
  
  test("will fail if interceptors unused", async () => {
    const mockClient = mockAgent.get("https://api.example.com");
    
    mockClient
      .intercept({ path: "/users", method: "GET" })
      .reply(200, []);
    
    mockClient
      .intercept({ path: "/posts", method: "GET" })
      .reply(200, []); // This won't be used
    
    // Only make one request
    await request("https://api.example.com/users");
    
    // assertNoPendingInterceptors will throw in afterEach
  });
});

Install with Tessl CLI

npx tessl i tessl/npm-undici-types

docs

connection-management.md

cookies.md

error-handling.md

http-api.md

http-clients.md

index.md

interceptors.md

testing-mocking.md

utilities.md

web-standards.md

tile.json