Testing
Arbor provides two testing helpers:
injectfor end-to-end tests against a full appcreateTestBundlefor direct action tests without routing
Both use the same internal machinery as the router, so tests behave like real requests.
Integration tests with inject
inject simulates a full HTTP request against an Arbor app without starting a server.
import { inject } from "@kequtech/arbor";
import app from "../src/app.ts";
test("GET /admin/dashboard reads the authorization header", async () => {
const { getResponse, res } = inject(app, {
method: "GET",
url: "/admin/dashboard",
headers: {
authorization: "mike",
},
});
const body = await getResponse();
expect(res.getHeader("Content-Type")).toBe("text/plain");
expect(body).toBe("Hello admin mike!");
});
Request options
inject(app, options) accepts the same ReqOptions Arbor uses internally:
method?: stringurl?: stringheaders?: Record<string, string>rawHeaders?: string[]body?: unknown
Example for JSON:
const { getResponse } = inject(app, {
method: "POST",
url: "/users",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ name: "April" }),
});
inject always finalizes the request automatically unless you pass body: null, doing so you can write manually using req for more fine grained testing control.
const { req, getResponse } = inject(app, {
method: "POST",
url: "/users",
headers: {
"content-type": "application/json",
},
body: null,
});
req.end(JSON.stringify({ name: "April" }));
getResponse
getResponse waits until the response is complete and then returns the body:
application/json→ parsed JSONtext/*→ string- anything else →
Buffer
const { getResponse, res } = inject(app, {
method: "GET",
url: "/info",
});
const body = await getResponse();
expect(res.statusCode).toBe(200);
expect(typeof body).toBe("object");
To always get a Buffer, use { raw: true }:
const data = await getResponse({ raw: true });
req and res are fake HTTP objects that behave like the real ones for testing.
Direct action tests with createTestBundle
For smaller tests you often do not want to involve routing or the full app. Instead, you can call an action directly with a test bundle.
Arbor exposes createTestBundle for this purpose.
import { createTestBundle } from "@kequtech/arbor/testing";
import actionUsers from "../src/app/api/users/action.ts";
test("users list returns payload", async () => {
const bundle = createTestBundle();
const result = await actionUsers(bundle);
expect(result).toEqual([{ id: 1, name: "User" }]);
});
TestBundleOptions
createTestBundle accepts TestBundleOptions, which extends ReqOptions and adds:
params?: Record<string, string>context?: BundleContext
Example with params and context:
const bundle = createTestBundle({
params: {
userId: "123",
},
context: {
requestId: "abc",
},
});
This produces the same Bundle object that inject would pass to the action:
reqandresurldefault/not necessarily used by all actionscontextdefault{}paramsdefault{}methodsdefault[method]cookiesandgetBodywired up correctly
Testing POST actions with getBody
Because createTestBundle uses the same getBody as real requests, you can test body parsing logic directly.
import { createTestBundle } from "@kequtech/arbor/testing";
import actionUsersCreate from "../src/app/api/users/create/action.ts";
test("create user parses body and returns response", async () => {
const bundle = createTestBundle({
method: "POST",
url: "/api/users",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ name: "April" }),
});
const result = await actionUsersCreate(bundle);
expect(result).toEqual({ created: "April" });
});
If your action relies on context, you can pre-populate it:
const bundle = createTestBundle({
method: "GET",
url: "/profile",
context: {
user: { id: "123", email: "user@example.com" },
},
});
const result = await actionProfile(bundle);
When to use which
- Use
injectwhen you want to test routing, branching, error handlers, renderers and other end-to-end behavior. - Use
createTestBundlewhen you want fast, focused tests for a single action.
Both tools are built on the same primitives as the runtime router, so you do not need a separate testing layer or spin up times.
Summary
Arbor's testing support is small but effective:
injectsimulates full HTTP traffic without a real servercreateTestBundlebuilds a realisticBundleso you can call actions directly- These reuse simulated req, simulated res, cookies and getBody internally
This keeps tests close to production behavior while remaining fast and easy to write as unit tests.