Skip to content

@connectum/testing

Testing utilities for the Connectum framework. Provides mock factories, assertion helpers, and a test server utility to eliminate boilerplate in ConnectRPC interceptor and service tests.

Layer: 2 (Testing Utilities)

Related Guides

Full API Reference

Complete TypeScript API documentation: API Reference

Installation

bash
pnpm add -D @connectum/testing

Requires: Node.js 18+

Peer dependencies: @connectrpc/connect, @bufbuild/protobuf

Quick Start

A typical interceptor unit test using @connectum/testing utilities:

typescript
import assert from 'node:assert';
import { describe, it } from 'node:test';
import { Code } from '@connectrpc/connect';
import {
  createMockRequest,
  createMockNext,
  createMockNextError,
  createMockNextSlow,
  assertConnectError,
} from '@connectum/testing';
import { createTimeoutInterceptor } from '@connectum/interceptors';

describe('timeout interceptor', () => {
  const interceptor = createTimeoutInterceptor({ duration: 100 });

  it('should pass through fast responses', async () => {
    const req = createMockRequest();
    const next = createMockNext({ message: { value: 42 } });

    const handler = interceptor(next);
    const res = await handler(req);

    assert.strictEqual(next.mock.calls.length, 1);
    assert.deepStrictEqual(res.message, { value: 42 });
  });

  it('should abort slow responses with DeadlineExceeded', async () => {
    const req = createMockRequest();
    const next = createMockNextSlow(500);

    const handler = interceptor(next);

    await assert.rejects(() => handler(req), (err: unknown) => {
      assertConnectError(err, Code.DeadlineExceeded);
      return true;
    });
  });

  it('should propagate upstream errors unchanged', async () => {
    const req = createMockRequest();
    const next = createMockNextError(Code.NotFound, 'User not found');

    const handler = interceptor(next);

    await assert.rejects(() => handler(req), (err: unknown) => {
      assertConnectError(err, Code.NotFound, 'User not found');
      return true;
    });
  });
});

API Reference

Mock Request

createMockRequest(options?)

Creates a mock ConnectRPC UnaryRequest for testing interceptors.

typescript
function createMockRequest(options?: MockRequestOptions): UnaryRequest;
OptionTypeDefaultDescription
servicestring'test.TestService'Service type name
methodstring'TestMethod'Method name
messageunknown{}Request message payload
streambooleanfalseStreaming request flag
urlstringAuto-generatedRequest URL
headersHeadersnew Headers()Request headers
typescript
// Minimal -- all defaults
const req = createMockRequest();
// → { url: 'http://localhost/test.TestService/TestMethod', stream: false, message: {}, ... }

// Custom service and message
const req = createMockRequest({
  service: 'myapp.UserService',
  method: 'GetUser',
  message: { id: '123' },
});

// Streaming request
const req = createMockRequest({ stream: true, message: createMockStream([{ id: '1' }, { id: '2' }]) });

Mock Next Functions

createMockNext(options?)

Creates a mock next handler returning a successful response. Returns a mock.fn() spy from node:test.

typescript
function createMockNext(options?: MockNextOptions): MockFunction;
OptionTypeDefaultDescription
messageunknown{ result: 'success' }Response message
streambooleanfalseStreaming response flag
typescript
import { createMockNext, createMockNextError, createMockNextSlow } from '@connectum/testing';
import { Code } from '@connectrpc/connect';

// Success
const next = createMockNext();
const result = await handler(req, next);
assert.strictEqual(next.mock.calls.length, 1);

// Custom response message
const next = createMockNext({ message: { id: 1, name: 'Alice' } });

// Error
const next = createMockNextError(Code.Internal, 'Database error');

// Slow (for timeout testing)
const next = createMockNextSlow(200, { message: { result: 'late' } });

createMockNextError(code, message?)

Creates a mock next handler that throws a ConnectError.

typescript
function createMockNextError(code: Code, message?: string): MockFunction;

createMockNextSlow(delay, options?)

Creates a mock next handler that responds after a delay. Useful for timeout testing.

typescript
function createMockNextSlow(delay: number, options?: MockNextOptions): MockFunction;

Assertions

assertConnectError(error, expectedCode, messagePattern?)

Type-safe assertion that narrows error to ConnectError. Checks the gRPC status code and optionally matches the message against a string or RegExp.

typescript
function assertConnectError(
  error: unknown,
  expectedCode: Code,
  messagePattern?: string | RegExp,
): asserts error is ConnectError;
typescript
// RegExp pattern matching
await assert.rejects(() => handler(req, next), (err: unknown) => {
  assertConnectError(err, Code.InvalidArgument, /validation failed/i);
  return true;
});

// String pattern matching
assertConnectError(err, Code.NotFound, 'user not found');

// Code-only check (no message matching)
assertConnectError(err, Code.PermissionDenied);

Protobuf Descriptor Mocks

createMockDescMessage(typeName, options?)

Creates a mock DescMessage with all required structural properties for protobuf utilities.

typescript
function createMockDescMessage(
  typeName: string,
  options?: MockDescMessageOptions,
): DescMessage;
OptionTypeDefaultDescription
fieldsArray<{ name, type?, fieldNumber? }>[]Field definitions
oneofsstring[][]Oneof group names
typescript
const schema = createMockDescMessage('test.UserMessage', {
  fields: [
    { name: 'id', type: 'int32' },
    { name: 'email', type: 'string' },
  ],
});

// Use in interceptor request
const req = createMockRequest({
  method: 'GetUser',
  message: { id: '123', email: '[email protected]' },
});
req.method.input = schema;
req.method.output = schema;

createMockDescField(localName, options?)

Creates a mock DescField descriptor.

typescript
function createMockDescField(
  localName: string,
  options?: MockDescFieldOptions,
): DescField;
OptionTypeDefaultDescription
isSensitivebooleanfalseMark field as sensitive (for redact interceptor)
fieldNumbernumberAuto-incrementedProto field number
typestring'string'Field scalar type
typescript
const passwordField = createMockDescField('password', { isSensitive: true });
const usernameField = createMockDescField('username');
const idField = createMockDescField('userId', { type: 'int32', fieldNumber: 1 });

createMockDescMethod(name, options?)

Creates a mock DescMethod descriptor. Auto-generates input/output message descriptors based on the method name.

typescript
function createMockDescMethod(
  name: string,
  options?: MockDescMethodOptions,
): DescMethod;
OptionTypeDefaultDescription
inputDescMessageAuto-generatedInput message descriptor
outputDescMessageAuto-generatedOutput message descriptor
kindstring'unary'Method kind (unary, server_streaming, client_streaming, bidi_streaming)
useSensitiveRedactionbooleanfalseEnable sensitive field redaction
typescript
const inputSchema = createMockDescMessage('test.LoginRequest');
const outputSchema = createMockDescMessage('test.LoginResponse');

const method = createMockDescMethod('Login', {
  input: inputSchema,
  output: outputSchema,
  useSensitiveRedaction: true,
});

// Streaming method
const streaming = createMockDescMethod('ListUsers', {
  kind: 'server_streaming',
});

Fake Service Descriptors

createFakeService(options?)

Creates a fake DescService descriptor for testing interceptors and utilities.

typescript
function createFakeService(options?: FakeServiceOptions): DescService;
OptionTypeDefaultDescription
typeNamestring'test.v1.TestService'Service type name
namestringDerived from typeNameService short name

createFakeMethod(service, name, options?)

Creates a fake DescMethod attached to a service. Use register: true to add it to the service's method list.

typescript
function createFakeMethod(
  service: DescService,
  name: string,
  options?: FakeMethodOptions,
): DescMethod;
OptionTypeDefaultDescription
methodKindstring'unary'Method kind
registerbooleanfalseRegister method in service.methods
typescript
const svc = createFakeService({ typeName: 'acme.v1.UserService' });
const getUser = createFakeMethod(svc, 'GetUser', { register: true });
const listUsers = createFakeMethod(svc, 'ListUsers', {
  methodKind: 'server_streaming',
  register: true,
});
// svc.methods.length === 2
// svc.method.getUser === getUser

Streaming

createMockStream(items, options?)

Creates a reusable AsyncIterable that yields items sequentially.

typescript
function createMockStream<T>(
  items: T[],
  options?: MockStreamOptions,
): AsyncIterable<T>;
OptionTypeDefaultDescription
delayMsnumberDelay between yielded items (ms)
typescript
const stream = createMockStream([{ id: '1' }, { id: '2' }]);
const slow = createMockStream([{ id: '1' }], { delayMs: 100 });

// In streaming interceptor test
const req = createMockRequest({
  stream: true,
  message: createMockStream([{ value: 'a' }, { value: 'b' }]),
});

Test Server

createTestServer(options)

Starts a real ConnectRPC server on a random port for integration testing. Returns a TestServer with a pre-configured gRPC transport.

typescript
function createTestServer(
  options: CreateTestServerOptions,
): Promise<TestServer>;
OptionTypeDefaultDescription
servicesunknown[]ConnectRPC service route handlers (required)
interceptorsunknown[][]Interceptors to apply
protocolsunknown[][]Protocol extensions (Healthcheck, Reflection)
portnumber0Port number (0 = random)

TestServer interface:

typescript
interface TestServer {
  transport: Transport;  // Pre-configured client transport
  baseUrl: string;       // e.g. 'http://localhost:54321'
  port: number;          // Assigned port number
  close(): Promise<void>; // Stop server and close connections
}
typescript
import { createTestServer } from '@connectum/testing';
import { createClient } from '@connectrpc/connect';
import { MyService } from './gen/myservice_pb.js';

describe('MyService integration', () => {
  let server: TestServer;

  beforeEach(async () => {
    server = await createTestServer({
      services: [myServiceRoutes],
      interceptors: [createValidationInterceptor()],
    });
  });

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

  it('should handle GetUser request', async () => {
    const client = createClient(MyService, server.transport);
    const response = await client.getUser({ id: '123' });
    assert.strictEqual(response.name, 'Test User');
  });
});

withTestServer(options, testFn)

Convenience wrapper that manages server lifecycle automatically -- starts before the test, closes after (even on error).

typescript
function withTestServer<T>(
  options: CreateTestServerOptions,
  testFn: (server: TestServer) => Promise<T>,
): Promise<T>;
typescript
it('should respond to requests', async () => {
  await withTestServer({ services: [myRoutes] }, async (server) => {
    const client = createClient(MyService, server.transport);
    const res = await client.getUser({ id: '1' });
    assert.strictEqual(res.name, 'Test User');
  });
});

Exports Summary

ExportCategoryDescription
createMockRequestMock RequestMock ConnectRPC unary request
createMockNextMock NextSuccessful response handler spy
createMockNextErrorMock NextError-throwing handler spy
createMockNextSlowMock NextDelayed response handler spy
assertConnectErrorAssertionsType-safe ConnectError assertion
createMockDescMessageProtobuf MocksMock message descriptor
createMockDescFieldProtobuf MocksMock field descriptor
createMockDescMethodProtobuf MocksMock method descriptor
createFakeServiceFake DescriptorsFake service descriptor
createFakeMethodFake DescriptorsFake method descriptor
createMockStreamStreamingMock async iterable stream
createTestServerTest ServerStart real test server
withTestServerTest ServerAuto-managed test server