Skip to content

Testing without a network

Use mockProvider or a replay ledger to give your asks repeatable answers without calling a live model. forceModel can route every Nola ask to that test model, including asks with their own model selection. The examples below use the typescript-interop template’s extractPerson function.

mockProvider accepts a queue of response values or a function that receives the request. Each provider call consumes one queue entry, so a validation correction needs another entry:

nola.config.ts
import { mockProvider, openai } from "@nola-lang/providers";
import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({
model: {
default: openai({ model: "gpt-5-mini" }),
mock: mockProvider([{ name: "Alice Smith", age: 32, employer: "Acme Corp", job: "engineer" }]),
echo: mockProvider((req) => ({ prompt: req.prompt.messages[0]?.content })),
},
// hermetic: with NOLA_TEST=1 EVERY ask goes to `mock`, even ones pinned with ask with / .withModel()
forceModel: process.env.NOLA_TEST === "1" ? "mock" : undefined,
});

forceModel overrides model selections made with ask with or .withModel(), including those inside dependencies. Mock replies still go through schema validation and the correction attempt, so you can test how your code handles invalid responses as well as valid ones.

Node’s built-in test runner can load .tsi files through the Nola loader:

// not-checked — test/person.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { extractPerson } from "../src/person.tsi";
test("extracts a person from prose", async () => {
const person = await extractPerson("Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.");
assert.equal(person.name, "Alice Smith");
});
Terminal window
NOLA_TEST=1 node --import nola-lang/register --test "test/**/*.test.ts"

The loader compiles .tsi in memory and applies nola.config.ts and .env before the tests run. Pass test file paths or a glob to the runner.

For a test-specific model, configure the runtime in the test and reset it afterward:

// not-checked — test/mock.test.ts
import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
import { mockProvider } from "@nola-lang/providers";
import { nolaRuntime } from "@nola-lang/runtime";
import { extractPerson } from "../src/person.tsi";
afterEach(() => nolaRuntime.reset());
test("configure in code with a mock provider", async () => {
nolaRuntime.reset(); // the loader may already have applied nola.config.ts — the config is frozen after the first ask
nolaRuntime.configure({ model: mockProvider([{ name: "Bob", age: 40, employer: "Initech", job: "dev" }]) });
const person = await extractPerson("whatever");
assert.equal(person.name, "Bob");
});

The configuration freezes after the first ask. Call reset() before replacing it; otherwise configure() throws NOLA3003.

Vitest uses Vite to transform modules. Add @nola-lang/vite to compile .tsi files and load nola.config.ts during tests:

Terminal window
npm i -D vitest @nola-lang/vite
// not-checked — vitest.config.ts
import { defineConfig } from "vitest/config";
import nola from "@nola-lang/vite";
export default defineConfig({
plugins: [nola()],
});
// not-checked — test/person.test.ts
import { expect, test } from "vitest";
import { extractPerson } from "../src/person.tsi";
test("extracts a person from prose", async () => {
const person = await extractPerson("Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.");
expect(person.name).toBe("Alice Smith");
});
Terminal window
NOLA_TEST=1 npx vitest run

To test with recorded model responses, capture a ledger during a live run and replay it in CI:

nola.config.ts
import { openai, record, replay } from "@nola-lang/providers";
import { defineConfig } from "@nola-lang/runtime";
const live = process.env.NOLA_RECORD === "1";
export default defineConfig({
model: live
? record(openai({ model: "gpt-5-mini" }), "./nola.replay.jsonl")
: replay("./nola.replay.jsonl"),
});

Commit nola.replay.jsonl. If a prompt, contextual value, type or parameter changes, the request fingerprint changes too. A request without a matching recording fails with NOLA3008. Run with NOLA_RECORD=1 to update the ledger, review the recorded responses and commit it. See Record and replay.

  • Assert on the typed value returned by an infer function. Avoid depending on exact prompt wording unless prompt composition is what you are testing.
  • To test routing or retries, register a hook and inspect receipt fields such as servedBy, attempts and fingerprint. See Observability.
  • Use valid response shapes for successful cases, and deliberately invalid responses to exercise correction and error handling.

Next: Error handling