@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
- Testing -- testing strategies and tools
- Custom Interceptors -- writing interceptors to test
Full API Reference
Complete TypeScript API documentation: API Reference
Installation
pnpm add -D @connectum/testingRequires: Node.js 18+
Peer dependencies: @connectrpc/connect, @bufbuild/protobuf
Quick Start
A typical interceptor unit test using @connectum/testing utilities:
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.
function createMockRequest(options?: MockRequestOptions): UnaryRequest;| Option | Type | Default | Description |
|---|---|---|---|
service | string | 'test.TestService' | Service type name |
method | string | 'TestMethod' | Method name |
message | unknown | {} | Request message payload |
stream | boolean | false | Streaming request flag |
url | string | Auto-generated | Request URL |
headers | Headers | new Headers() | Request headers |
// 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.
function createMockNext(options?: MockNextOptions): MockFunction;| Option | Type | Default | Description |
|---|---|---|---|
message | unknown | { result: 'success' } | Response message |
stream | boolean | false | Streaming response flag |
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.
function createMockNextError(code: Code, message?: string): MockFunction;createMockNextSlow(delay, options?)
Creates a mock next handler that responds after a delay. Useful for timeout testing.
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.
function assertConnectError(
error: unknown,
expectedCode: Code,
messagePattern?: string | RegExp,
): asserts error is ConnectError;// 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.
function createMockDescMessage(
typeName: string,
options?: MockDescMessageOptions,
): DescMessage;| Option | Type | Default | Description |
|---|---|---|---|
fields | Array<{ name, type?, fieldNumber? }> | [] | Field definitions |
oneofs | string[] | [] | Oneof group names |
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.
function createMockDescField(
localName: string,
options?: MockDescFieldOptions,
): DescField;| Option | Type | Default | Description |
|---|---|---|---|
isSensitive | boolean | false | Mark field as sensitive (for redact interceptor) |
fieldNumber | number | Auto-incremented | Proto field number |
type | string | 'string' | Field scalar type |
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.
function createMockDescMethod(
name: string,
options?: MockDescMethodOptions,
): DescMethod;| Option | Type | Default | Description |
|---|---|---|---|
input | DescMessage | Auto-generated | Input message descriptor |
output | DescMessage | Auto-generated | Output message descriptor |
kind | string | 'unary' | Method kind (unary, server_streaming, client_streaming, bidi_streaming) |
useSensitiveRedaction | boolean | false | Enable sensitive field redaction |
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.
function createFakeService(options?: FakeServiceOptions): DescService;| Option | Type | Default | Description |
|---|---|---|---|
typeName | string | 'test.v1.TestService' | Service type name |
name | string | Derived from typeName | Service 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.
function createFakeMethod(
service: DescService,
name: string,
options?: FakeMethodOptions,
): DescMethod;| Option | Type | Default | Description |
|---|---|---|---|
methodKind | string | 'unary' | Method kind |
register | boolean | false | Register method in service.methods |
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 === getUserStreaming
createMockStream(items, options?)
Creates a reusable AsyncIterable that yields items sequentially.
function createMockStream<T>(
items: T[],
options?: MockStreamOptions,
): AsyncIterable<T>;| Option | Type | Default | Description |
|---|---|---|---|
delayMs | number | — | Delay between yielded items (ms) |
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.
function createTestServer(
options: CreateTestServerOptions,
): Promise<TestServer>;| Option | Type | Default | Description |
|---|---|---|---|
services | unknown[] | — | ConnectRPC service route handlers (required) |
interceptors | unknown[] | [] | Interceptors to apply |
protocols | unknown[] | [] | Protocol extensions (Healthcheck, Reflection) |
port | number | 0 | Port number (0 = random) |
TestServer interface:
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
}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).
function withTestServer<T>(
options: CreateTestServerOptions,
testFn: (server: TestServer) => Promise<T>,
): Promise<T>;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
| Export | Category | Description |
|---|---|---|
createMockRequest | Mock Request | Mock ConnectRPC unary request |
createMockNext | Mock Next | Successful response handler spy |
createMockNextError | Mock Next | Error-throwing handler spy |
createMockNextSlow | Mock Next | Delayed response handler spy |
assertConnectError | Assertions | Type-safe ConnectError assertion |
createMockDescMessage | Protobuf Mocks | Mock message descriptor |
createMockDescField | Protobuf Mocks | Mock field descriptor |
createMockDescMethod | Protobuf Mocks | Mock method descriptor |
createFakeService | Fake Descriptors | Fake service descriptor |
createFakeMethod | Fake Descriptors | Fake method descriptor |
createMockStream | Streaming | Mock async iterable stream |
createTestServer | Test Server | Start real test server |
withTestServer | Test Server | Auto-managed test server |
Related Packages
- @connectum/core -- server foundation used by
createTestServer - @connectum/interceptors -- interceptors commonly tested with these utilities
- @connectum/auth -- auth interceptors testable with mock request/next
