Testing
You don't need a running server to test a Lyn app. app.handle(request) takes a standard Request and returns the Response the server would send: listen() goes through exactly the same code. Tests are fast, use no port, and there is nothing to stop.
A first test
// users.test.ts
import { expect, it } from "bun:test";
import { Lyn, z } from "@rouret/lyn";
const app = new Lyn().post(
"/users",
({ body }) => ({ name: body.name }),
{ body: z.object({ name: z.string() }) }
);
it("creates a user", async () => {
const response = await app.handle(
new Request("http://localhost/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Ada" }),
})
);
expect(response.status).toBe(201);
expect(await response.json()).toEqual({ name: "Ada" });
});NODE_ENV=test LOG_LEVEL=silent bun testLOG_LEVEL=silent keeps the test output clean. The host in the URL is ignored: only the method, path, query string, headers and body matter.
A small helper
Building Request objects gets repetitive. A ten-line helper is enough:
// test/client.ts
import type { Lyn } from "@rouret/lyn";
export const createClient = (app: Pick<Lyn, "handle">) => {
const send = (method: string, path: string, body?: unknown) =>
app.handle(
new Request(`http://localhost${path}`, {
method,
...(body !== undefined && {
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
}),
})
);
return {
get: (path: string) => send("GET", path),
post: (path: string, body?: unknown) => send("POST", path, body),
put: (path: string, body?: unknown) => send("PUT", path, body),
delete: (path: string) => send("DELETE", path),
};
};const client = createClient(app);
const response = await client.post("/users", { nope: true });
expect(response.status).toBe(400);With a database
Point the app at a test database with url, and close it after the tests:
import { afterAll, expect, it } from "bun:test";
import * as schema from "../src/db/schema";
const app = new Lyn({
env: { testDatabaseUrl: { name: "TEST_DATABASE_URL", type: "string" } },
database: { schema, url: (env) => env.testDatabaseUrl },
}).get("/users", ({ db }) => db.query.users.findMany());
afterAll(() => app.stop());
it("lists users", async () => {
const response = await app.handle(new Request("http://localhost/users"));
expect(response.status).toBe(200);
});No listen() is needed: the connection opens on the first query.
Environment variables
Variables are read in new Lyn(). Set them on Bun.env before creating the app, and restore them afterwards, since all test files share the same process.
Removing a variable
Use delete Bun.env.NAME. Assigning undefined stores the string "undefined".
An invalid configuration throws, so you can test it without mocks:
import { Lyn, LynEnvError } from "@rouret/lyn";
delete Bun.env.API_KEY;
expect(() => new Lyn({ env: { apiKey: { name: "API_KEY", type: "string" } } }))
.toThrow(LynEnvError);Over real HTTP
To test the real server, call listen() without a port to get a free one:
const server = await app.listen();
try {
const response = await fetch(`${server.url}/users`);
expect(response.status).toBe(200);
} finally {
await server.stop();
}You rarely need it: handle() covers the same behaviour.