Skip to content

Getting Started with Testing

Kyrin comes with a built-in testing toolkit that lets you test your routes, middleware, and request hooks without spinning up a real HTTP server. Everything runs in-memory for fast, predictable tests.

Installation

The test utilities are built into Kyrin — no additional dependencies are needed.

typescript
import { createApp, TestApp, TestClient } from "kyrin";

Your First Test

typescript
import { createApp, expectStatus, expectBody } from "kyrin";

const app = createApp();

app.get("/hello", () => ({ message: "Hello, World!" }));

const client = app.getClient();
const res = await client.get("/hello");

expectStatus(res.status).toBe(200);
expectBody(res.body).toEqual({ message: "Hello, World!" });

TestApp

TestApp is a lightweight, in-process application that mimics Kyrin's production router and middleware system. Create one with createApp().

Creating an App

typescript
const app = createApp();

Registering Routes

typescript
app.get("/users", () => ({ users: ["Alice", "Bob"] }));
app.get("/users/:id", (c) => ({ id: c.param("id") }));
app.post("/users", async (c) => {
  const body = await c.body();
  return { created: true, ...body };
});
app.put("/users/:id", (c) => ({ updated: c.param("id") }));
app.patch("/users/:id", (c) => ({ patched: c.param("id") }));
app.delete("/users/:id", () => ({ deleted: true }));

// Custom HTTP method
app.on("GET", "/custom", (c) => ({ method: "GET" }));

// Match any method
app.all("/fallback", (c) => ({ fallback: true }));

Adding Middleware

typescript
app.use(async (c, next) => {
  c.set.headers["X-Request-Id"] = "test-123";
  await next();
});

// Multiple middlewares run in order
app.use(async (c, next) => {
  console.log("Before handler");
  await next();
  console.log("After handler");
});

Request Hooks

Request hooks let you intercept or modify requests before they reach your route handler.

typescript
app.onRequest((c) => {
  // Modify the request before it's processed
  c.req.headers.set("Authorization", "Bearer test-token");
});

// Return early without hitting the handler
app.onRequest((c) => {
  if (c.path === "/blocked") {
    return new Response("Blocked", { status: 403 });
  }
});

TestClient

TestClient provides convenience methods for making HTTP requests against your TestApp.

Creating a Client

typescript
const client = app.getClient();

Making Requests

typescript
// GET
const res = await client.get("/users");
const res = await client.get("/users?page=1&limit=10");

// POST with body
const res = await client.post("/users", { name: "Alice" });

// POST with body and custom headers
const res = await client.post("/users", { name: "Alice" }, {
  Authorization: "Bearer token123",
});

// PUT
const res = await client.put("/users/1", { name: "Bob" });

// PATCH
const res = await client.patch("/users/1", { name: "Bob" });

// DELETE
const res = await client.delete("/users/1");

TestResponse

Every request returns a TestResponse with a predictable shape:

typescript
interface TestResponse {
  status: number;                   // HTTP status code
  headers: Record<string, string>;  // Response headers
  body: unknown;                    // Parsed response body
}

const res = await client.get("/users");

console.log(res.status);   // 200
console.log(res.headers);  // { "content-type": "application/json" }
console.log(res.body);     // { users: ["Alice", "Bob"] }

Complete Test Example

typescript
import { createApp, expectStatus, expectBody } from "kyrin";

describe("User API", () => {
  let app: ReturnType<typeof createApp>;
  let client: ReturnType<ReturnType<typeof createApp>["getClient"]>;

  beforeEach(() => {
    app = createApp();

    // Middleware
    app.use(async (c, next) => {
      c.set.headers["X-Test"] = "true";
      await next();
    });

    // Routes
    app.get("/users", () => ({ users: ["Alice", "Bob"] }));
    app.get("/users/:id", (c) => ({ id: c.param("id") }));
    app.post("/users", async (c) => {
      const body = await c.body();
      return { created: true, ...body };
    });

    // Request hook
    app.onRequest((c) => {
      if (c.path === "/secret") {
        return new Response("Forbidden", { status: 403 });
      }
    });

    client = app.getClient();
  });

  test("GET /users returns user list", async () => {
    const res = await client.get("/users");
    expectStatus(res.status).toBe(200);
    expectBody(res.body).toEqual({ users: ["Alice", "Bob"] });
  });

  test("GET /users/:id returns the id param", async () => {
    const res = await client.get("/users/123");
    expectStatus(res.status).toBe(200);
    expectBody(res.body).toEqual({ id: "123" });
  });

  test("POST /users creates a user", async () => {
    const res = await client.post("/users", { name: "NewUser" });
    expectStatus(res.status).toBe(200);
    expectBody(res.body).toEqual({ created: true, name: "NewUser" });
  });

  test("onRequest hook can block paths", async () => {
    const res = await client.get("/secret");
    expectStatus(res.status).toBe(403);
    expectBody(res.body).toBe("Forbidden");
  });
});

Next Steps

Released under the MIT License.