Skip to content

Write your first test

Drive a Flare app through app.test() and assert on the Response, no port bound.

AI generated, pending review Updated 10 days ago · Flare 0.3

You want to test a route the way a request hits it in production: through routing, middleware, and the handler, asserting on the real Response. Build the host under FLARE_MODE=test, get a handle from app.test(), send a synthetic request with handle.fetch, and check the status and JSON body.

  1. Set FLARE_MODE=test before the host module loads.
  2. Build the app and await app.test() to get a TestAppHandle.
  3. Send "METHOD /path" with handle.fetch and assert on the returned Response.
import { FlareHost, FlareResponse } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
import { strict as assert } from "node:assert";
// FLARE_MODE=test must be set before this module loads (see Notes).
const host = new FlareHost(node);
host.http.get("/ping", () => new FlareResponse(200, { ok: true }));
const app = host.build();
const handle = await app.test();
const res: Response = await handle.fetch("GET /ping");
assert.equal(res.status, 200);
assert.deepEqual(await res.json(), { ok: true });
await handle.stop();

handle.fetch walks the same HTTP pipeline app.run() would, minus the socket bind. The target is a "METHOD /path" string ("GET /ping"). The method is uppercased and the path must start with /. The call resolves to a standard Web Response.

Pass body in the second argument. Anything that is not raw bytes or a string is JSON-stringified, and content-type: application/json is set when you do not set it yourself.

import { FlareHost, FlareResponse } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
import { schema, str } from "@flare-ts/lib/schema";
import { strict as assert } from "node:assert";
const host = new FlareHost(node);
host.http.post(
"/echo",
{ body: schema({ name: str }) },
(_ctx, scope) => new FlareResponse(201, { greeting: `hi ${scope.input.body!.name}` }),
);
const handle = await host.build().test();
const res = await handle.fetch("POST /echo", { body: { name: "ada" } });
assert.equal(res.status, 201);
assert.deepEqual(await res.json(), { greeting: "hi ada" });
await handle.stop();

The route declares a body descriptor, so scope.input.body is typed and validated before the handler runs. A malformed payload is rejected with 400 before your handler sees it.

Build the app once in setup, reuse the handle across cases, and call handle.stop() at the end so singleton onStop() hooks run.

// @ground:skip
import { afterAll, beforeAll, expect, it } from "vitest";
import type { TestAppHandle } from "@flare-ts/core/testing";
import { host } from "../src/host.js";
let handle: TestAppHandle;
beforeAll(async () => {
handle = await host.build().test();
});
afterAll(async () => {
await handle.stop();
});
it("returns ok on /ping", async () => {
const res = await handle.fetch("GET /ping");
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ ok: true });
});
  • FLARE_MODE=test must be set before the host module is imported. The simplest place is your runner’s env config ({ test: { env: { FLARE_MODE: "test" } } } in vitest.config.ts). Without it, host.build() returns the production app and app.test() throws.
  • app.test() runs once per host instance. To swap a service between scenarios in one file, use handle.reset({ replace }), not a second app.test().
  • The test path keeps middleware, error handlers, serializers, and route contracts. It only skips the socket bind and the long-lived process.
  • Every fetch response includes an x-request-id header (test-1, test-2, and so on per handle).