Testing
Test your custom RouteCraft routes with fast unit tests and optional E2E runs.
Quick start
import { describe, it, expect, vi } from "vitest";
import { context } from "@routecraft/routecraft";
import helloRoute from "../routes/hello-world.route";
describe("hello route", () => {
it("emits and logs", async () => {
const logSpy = vi.spyOn(console, "log");
const ctx = context().routes(helloRoute).build();
const execution = ctx.start();
await new Promise((r) => setTimeout(r, 100));
await ctx.stop();
await execution;
expect(logSpy).toHaveBeenCalled();
});
});
Vitest configuration
For a new project, use a single vitest.config.mjs at the project root:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "node",
coverage: { provider: "v8", reporter: ["text", "lcov"] },
},
});
Route lifecycle in tests
Build a CraftContext, start it, give it time to run, then stop and await completion:
import { context } from "@routecraft/routecraft";
import routes from "../routes/hello-world.route"; // your route builder export
const testContext = context().routes(routes).build();
const execution = testContext.start();
await new Promise((r) => setTimeout(r, 100));
await testContext.stop();
await execution;
Checklist:
- Start with
const execution = ctx.start(); laterawait ctx.stop()andawait execution. - Keep waits small (50–200ms) for single-shot routes; use timers/mocks for long-running routes.
- Restore mocks in
beforeEach/afterEach.
Common testing patterns
Using the spy adapter
The spy() adapter is purpose-built for testing. It records all interactions and provides convenient assertion methods:
import { spy } from "@routecraft/routecraft";
const spyAdapter = spy();
// Available properties:
spyAdapter.received // Array of exchanges received
spyAdapter.calls.send // Number of send() calls
spyAdapter.calls.process // Number of process() calls (if used as processor)
spyAdapter.calls.enrich // Number of enrich() calls (if used as enricher)
// Methods:
spyAdapter.reset() // Clear all recorded data
spyAdapter.lastReceived() // Get the most recent exchange
spyAdapter.receivedBodies() // Get array of just the body values
Spy on destinations to assert outputs
import { craft, simple, spy, context } from "@routecraft/routecraft";
import { expect } from "vitest";
const spyAdapter = spy();
const route = craft().id("out").from(simple("payload")).to(spyAdapter);
const ctx = context().routes(route).build();
await ctx.start();
expect(spyAdapter.received).toHaveLength(1);
expect(spyAdapter.received[0].body).toBe("payload");
expect(spyAdapter.calls.send).toBe(1);
Spy on console logs
For routes that use .to(log()), spy on console.log to verify logging behavior:
import { craft, simple, log, context } from "@routecraft/routecraft";
import { vi, expect } from "vitest";
test('logs messages correctly', async () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const route = craft()
.id("log-test")
.from(simple("Hello, World!"))
.to(log());
const ctx = context().routes(route).build();
await ctx.start();
expect(logSpy).toHaveBeenCalled();
const loggedMessage = logSpy.mock.calls[0][0];
expect(loggedMessage).toContain("Hello, World!");
logSpy.mockRestore();
});
Tip: Use spy() adapter instead of log() when you need more control over assertions.
Mock child logger for timer-heavy tests:
import { vi } from "vitest";
import { logger } from "@routecraft/routecraft";
const childLogger = { info: vi.fn(), debug: vi.fn(), warn: vi.fn(), error: vi.fn(), level: "info", child: vi.fn().mockReturnThis() } as any;
vi.spyOn(logger, "child").mockReturnValue(childLogger);
Filter logs by route id (from LogAdapter headers):
const logsForRoute = calls.filter(
(arg) => typeof arg === "object" && arg != null && "headers" in arg && (arg as any).headers?.["routecraft.route"] === "channel-adapter-1",
);
Test custom sources that await the final exchange
import { craft, context, spy } from "@routecraft/routecraft";
let observed: any;
const spyAdapter = spy();
const route = craft()
.id("return-final")
.from({
subscribe: async (_ctx, handler, controller) => {
try {
observed = await handler("hello");
} finally {
controller.abort();
}
},
},
])
.transform((body: string) => body.toUpperCase())
.to(spyAdapter)
.transform((body: string) => `${body}!`);
const ctx = context().routes(route).build();
await ctx.start();
expect(observed.body).toBe("HELLO!");
expect(spyAdapter.received[0].body).toBe("HELLO!");
Timers and long‑running routes
Option A: small real waits (simple):
const execution = ctx.start();
await new Promise((r) => setTimeout(r, 150));
await ctx.stop();
await execution;
Assertion patterns
Spy adapter assertions
// Basic assertions
expect(spyAdapter.received).toHaveLength(3);
expect(spyAdapter.calls.send).toBe(3);
// Body content validation
expect(spyAdapter.receivedBodies()).toEqual(['msg1', 'msg2', 'msg3']);
expect(spyAdapter.lastReceived().body).toBe('final-message');
// Header validation
expect(spyAdapter.received[0].headers['routecraft.route']).toBe('my-route');
// Complex object validation
const lastExchange = spyAdapter.lastReceived();
expect(lastExchange.body).toHaveProperty("original");
expect(lastExchange.body).toHaveProperty("additional");
Using spy as processor or enricher
// Test processing behavior
const processSpy = spy();
const route = craft()
.id("test-process")
.from(simple("input"))
.process(processSpy) // Use spy as processor
.to(spy());
await ctx.start();
expect(processSpy.calls.process).toBe(1);
expect(processSpy.received[0].body).toBe("input");
// Test enrichment behavior
const enrichSpy = spy();
const route2 = craft()
.id("test-enrich")
.from(simple({ name: "John" }))
.enrich(enrichSpy) // Use spy as enricher
.to(spy());
await ctx.start();
expect(enrichSpy.calls.enrich).toBe(1);
Route validation
// Ensure a route id is set after build
const r = craft().id("x").from(simple("y")).to(spy());
expect(r.build()[0].id).toBe("x");
Multiple spies in one route
const transformSpy = spy();
const destinationSpy = spy();
const route = craft()
.id("multi-spy")
.from(simple("start"))
.process(transformSpy)
.to(destinationSpy);
await ctx.start();
// Verify the pipeline
expect(transformSpy.calls.process).toBe(1);
expect(destinationSpy.calls.send).toBe(1);
expect(transformSpy.received[0].body).toBe("start");
expect(destinationSpy.received[0].body).toBe("start"); // Assuming spy processes pass-through
Headers and correlation
const captured: string[] = [];
// inside a .process/.tap
captured.push(exchange.headers["routecraft.correlation_id"] as string);
expect(new Set(captured).size).toBe(1);
Run route files
Use the CLI to run compiled route files/folders as an integration check:
pnpm craft run ./examples/hello-world.mjs
Troubleshooting
- Hanging tests: ensure you
await ctx.stop()and thenawait execution. - Flaky timers: prefer fake timers or increase the wait to 100–200ms.
- No logs captured: ensure your route includes
.to(log())or you spy on the child logger.