Skip to content

Core Concepts

This page covers the fundamental concepts you need to understand to use Testurio effectively.

The Three Roles

Every Testurio test is built around three primary roles:

RoleWhat it doesComponent
ClientSends requests/messages and asserts on responsesClient, AsyncClient
MockIntercepts requests, validates payloads, returns controlled responsesServer (listenAddress only), AsyncServer
ProxyForwards traffic to a backend while allowing inspection, transformation, and selective mockingServer (listenAddress + targetAddress), AsyncServer
typescript
// Client — sends requests
const api = new Client('api', {
  protocol: new HttpProtocol(),
  targetAddress: { host: 'localhost', port: 3000 },
});

// Mock — returns controlled responses
const mock = new Server('mock', {
  protocol: new HttpProtocol(),
  listenAddress: { host: 'localhost', port: 3000 },
});

// Proxy — forwards to backend with interception
const proxy = new Server('proxy', {
  protocol: new HttpProtocol(),
  listenAddress: { host: 'localhost', port: 3001 },
  targetAddress: { host: 'localhost', port: 3000 }, // ← makes it a proxy
});

These three roles work the same way across all protocols (HTTP, gRPC, WebSocket, TCP). Additional components — Publisher, Subscriber, and DataSource — supplement the model for message queues and databases.

Components

Components represent participants in your distributed system. Each component wraps a protocol adapter and manages its lifecycle (start/stop).

ComponentProtocol TypeRole
ClientSync (HTTP, gRPC Unary)Sends requests to a target server
ServerSyncMock server or proxy (depends on config)
AsyncClientAsync (WebSocket, TCP, gRPC Stream)Sends messages over persistent connections
AsyncServerAsyncMock async server or proxy (depends on config)
PublisherMQ AdapterPublishes messages to topics
SubscriberMQ AdapterSubscribes to and asserts on messages
DataSourceDirect SDKExecutes operations on databases/caches

Components are created once and reused across test cases within a scenario.

Protocols

Protocols are stateless adapter factories. They know how to create server and client adapters but hold no state themselves. The component owns the adapter.

typescript
// The protocol creates adapters; the Client component owns them
const client = new Client('api', {
  protocol: new HttpProtocol<UserApi>(),
  targetAddress: { host: 'localhost', port: 3000 },
});
ProtocolTypePackage
HttpProtocolSynctesturio (built-in)
GrpcUnaryProtocolSync@testurio/protocol-grpc
GrpcStreamProtocolAsync@testurio/protocol-grpc
WebSocketProtocolAsync@testurio/protocol-ws
TcpProtocolAsync@testurio/protocol-tcp

Test Scenario

A TestScenario groups components and manages their lifecycle. It starts servers before clients, and stops them in reverse order.

typescript
const scenario = new TestScenario({
  name: 'My Test Suite',
  components: [server, client],  // servers first, then clients
});

Component startup order:

  1. Non-network components (DataSource, Publisher, Subscriber)
  2. Servers (sequentially, in array order)
  3. Clients (in parallel)

Test Case

A testCase() declares the steps of a single test. Inside the callback, you use test.use(component) to get a typed step builder for each component:

typescript
const tc = testCase('my test', (test) => {
  const api = test.use(client);     // SyncClientStepBuilder
  const mock = test.use(server);    // SyncServerStepBuilder

  api.request('getUsers', { method: 'GET', path: '/users' });
  mock.onRequest('getUsers').mockResponse(() => ({ code: 200, body: [] }));
  api.onResponse('getUsers').assert((res) => res.code === 200);
});

Declarative only

Inside testCase(), only use builder methods. No imperative code — no await, no if/else, no loops. Steps are collected during the builder phase and executed later.

Step Modes

Every step has a mode that controls when and how it executes:

ModeBehaviorExamples
actionExecutes immediately (sends a request, publishes a message)request(), connect(), sendMessage(), disconnect(), publish(), exec()
hookRegisters a handler that fires when a matching message arrives. Does not block.onRequest(), onResponse(), onMessage()
waitBlocks execution until a matching message arrives or times out.waitResponse(), waitMessage(), waitConnection()

Three-Phase Execution

When you call scenario.run(tc), the test case executes in three phases:

  1. Phase 1 — Register hooks: All hooks from all steps are registered on their components before any step executes. This ensures mock handlers are in place before requests are sent.

  2. Phase 2 — Execute steps: Steps run sequentially in declaration order. Action steps fire immediately. Hook steps are no-ops (already registered). Wait steps block until their hook resolves.

  3. Phase 3 — Cleanup: All hooks are cleared from components.

This model means you can write steps in logical message flow order without worrying about timing.

Hooks

Hooks are the mechanism for intercepting and responding to messages. You build them using the fluent API:

typescript
// Server hook: intercept request and return a mock response
mock.onRequest('getUser').mockResponse(() => ({ code: 200, body: { id: 1 } }));

// Client hook: assert on the response
api.onResponse('getUser').assert((res) => res.body.id === 1);

// Async server hook: respond to a message with an event
wsMock.onMessage('ping').mockEvent('pong', (msg) => ({ seq: msg.seq }));

Available hook handlers:

HandlerDescription
.assert(fn)Validate the payload; fail the test if false
.assert(description, fn)Assert with a named description for error messages
.mockResponse(fn)Return a mock response (sync server only)
.mockEvent(type, fn)Send an event back (async server only)
.transform(fn)Transform the payload before forwarding
.delay(ms)Add a delay before processing
.drop()Drop the message entirely
.proxy(fn)Forward to backend with optional transformation (proxy mode)
.validate()Validate payload against the registered schema

Typing Modes

Testurio supports three ways to type your protocols:

Loose Mode

No type parameter — any string accepted as operation ID:

typescript
new HttpProtocol()

Explicit Generic Mode

Provide a TypeScript interface — only defined operations accepted:

typescript
new HttpProtocol<UserApi>()

Schema-First Mode

Provide Zod schemas — types inferred automatically with runtime validation:

typescript
new HttpProtocol({ schema: userApiSchema })

Proxy Mode

When a Server or AsyncServer has both listenAddress and targetAddress, it acts as a proxy:

typescript
const proxy = new Server('gateway', {
  protocol: new HttpProtocol(),
  listenAddress: { host: 'localhost', port: 3001 },
  targetAddress: { host: 'localhost', port: 3000 },  // forwards to backend
});

In proxy mode, hooks intercept messages flowing through the proxy, allowing you to inspect, transform, mock, or drop traffic.

Next Steps

Released under the MIT License.