Search docs

Jump between documentation pages.

Testing & contract tests

In-process test client

Every App exposes a request() method that round-trips a fetch Request through the same pipeline real traffic uses — no socket, no port:

ts
import test from "node:test";
import assert from "node:assert/strict";
import { app } from "../src/server.js";

test("GET /books/1 returns 200", async () => {
  const res = await app.request("/books/1");
  assert.equal(res.status, 200);
  assert.equal((await res.json()).title, "Foundation");
});

test("POST /books rejects unauthorized", async () => {
  const res = await app.request("/books", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ title: "Dune" }),
  });
  assert.equal(res.status, 401);
});

Mock mode

For pure-contract testing (no DB, no side effects), enable mockMode. DaloyJS will return the first declared examples entry from your response schema without ever invoking your handler:

ts
const app = new App({ mockMode: true });

app.route({
  method: "GET",
  path: "/users/:id",
  operationId: "getUser",
  responses: {
    200: {
      description: "ok",
      body: z.object({ id: z.string(), name: z.string() }),
      examples: { default: { id: "u_1", name: "Alice" } },
    },
  },
  handler: async () => { throw new Error("not called in mock mode"); },
});

Contract test runner

runContractTests walks your registered routes and verifies that every declared example validates against its schema, every operationId is unique, and there are no obvious anti-patterns:

ts
import { runContractTests } from "@daloyjs/core/contract";

const report = await runContractTests(app, {
  requireOperationId: true,
  allowBodyOnSafeMethods: false,
});

if (!report.ok) {
  console.error(report.issues);
  process.exit(1);
}
console.log(`${report.checked} routes — all clean`);

The report flags:

  • Routes missing operationId.
  • Duplicate operationIds.
  • Examples that don't match their schemas.
  • Body schemas declared on safe methods (GET, HEAD, DELETE).
  • Routes with no declared responses.

Wire into CI

json
{
  "scripts": {
    "test":      "node --import tsx/esm --test tests/**/*.test.ts",
    "test:contract": "node --import tsx/esm scripts/contract.ts"
  }
}