# Testing & contract tests

**Diagram: Three layers, one app**

1. **In-process tests** - app.request() through the real pipeline
2. **Mock mode** - new App({ mockMode: true }), returns examples
3. **Contract runner** - runContractTests(app) validates every route
4. **CI / pre-push gate** (backstop) - daloy inspect --check, exits non-zero

In-process tests exercise real handlers through app.request(), mock mode returns declared examples for pure contract assertions, and the contract runner validates the whole spec, the same check daloy inspect runs in CI and the pre-push hook.

## In-process test client

Every `App` exposes a `request()` method that round-trips a fetch `Request` through the same pipeline real traffic uses, without opening a socket or binding a 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);
});
```

## Typed in-process client

When the test should use operation ids and contract-derived inputs, build a `createInProcessClient(app)`. It opens no socket but still traverses routing, authentication, validation, hooks, and response serialization.

```ts
import { createInProcessClient } from "@daloyjs/core/client";
import { app } from "../src/app.js";

const client = createInProcessClient(app);

test("getBook is typed end to end", async () => {
  const response = await client.getBook({ params: { id: "book_1" } });
  assert.equal(response.status, 200);
  if (response.status === 200) {
    assert.equal(response.body.id, "book_1"); // body is narrowed and typed
  }
});
```

## Mock mode

For pure-contract testing (no DB, no side effects), enable `mockMode`. DaloyJS returns the first declared response `examples` entry for the first 2xx status without ever invoking your handler. If no example is declared, the mocked body is `null`.

```ts
import { App } from "@daloyjs/core";
import { z } from "zod";

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

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

## Contract test runner

`runContractTests` walks your registered routes and verifies that every declared response and `meta.examples` payload validates against its schema, every required `operationId` is present and 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`);
```

**Diagram: What the runner checks**

- **runContractTests(app)** (source, one pass) - walks every registered route
- **operationId present** - requireOperationId
- **operationIds unique** - no duplicates across routes
- **examples match schema** - response examples + meta.examples
- **no body on safe methods** - GET / HEAD / DELETE warning
- **responses declared** - every route has responses
- **report.ok** (converge) - false only for error-level issues

A single walk over your routes produces one report. Error-level issues make report.ok false. Warnings, such as safe-method body schemas, stay in report.issues without failing the gate.

The report flags:

- Routes missing `operationId`.
- Duplicate operationIds.
- Response examples and `meta.examples` payloads that don't match their schemas.
- Body schemas declared on safe methods (`GET`, `HEAD`, `DELETE`) as warnings.
- Routes with no declared `responses`.

## Wire into CI

```json
{
  "scripts": {
    "test": "node --test tests/**/*.test.ts",
    "contract": "daloy inspect --check src/build-app.ts"
  }
}
```

Or skip the script and let the CLI do it directly. `daloy inspect --check <entry>` loads your app, runs the same checks, and exits non-zero on any error-level issue, so it drops straight into a CI step. The entry must export your `App` as the default export or a named `app` export.

```bash
pnpm exec daloy inspect --check src/build-app.ts
```

Every `create-daloy` template already ships this gate: a `tests/contract.test.ts` (`tests/contract_test.ts` on Deno) that asserts `report.ok` for the real app and proves the gate rejects a broken contract. It runs as part of the project's `test` task, so a missing operationId or a mismatched example fails CI from the first commit.

## Gate it locally with a pre-push hook

A contract check is an authoring-time concern, so it belongs on your machine, never on the production request path. A `pre-push` git hook is the cleanest home for it: it is localhost-only by construction (it cannot run in production), adds no server boot cost, and fires right before code leaves your machine, with CI as the backstop.

Every `create-daloy` template ships this hook under `.githooks/pre-push`, wired to a `hooks:install` script. Enabling it is one command per clone: it points `core.hooksPath` at the committed hook, so the whole team shares the same gate. The hook skips gracefully when tooling is missing (it never blocks a push over an uninstalled dependency), and you can always bypass it once with `git push --no-verify`.

```bash
# Enable the contract gate for this clone (points core.hooksPath at .githooks)
pnpm hooks:install

# From then on, every `git push` runs the contract check first:
#   .githooks/pre-push  ->  daloy inspect --check src/build-app.ts
# Need to push past it once:
git push --no-verify
```

---

Source: https://daloyjs.dev/docs/testing