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
Section titled “mockProvider”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:
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.
A test under the loader (node:test)
Section titled “A test under the loader (node:test)”Node’s built-in test runner can load .tsi files through the Nola loader:
// not-checked — test/person.test.tsimport 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");});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.
Configuring in code
Section titled “Configuring in code”For a test-specific model, configure the runtime in the test and reset it afterward:
// not-checked — test/mock.test.tsimport 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
Section titled “vitest”Vitest uses Vite to transform modules. Add @nola-lang/vite to compile .tsi files and load nola.config.ts during tests:
npm i -D vitest @nola-lang/vite// not-checked — vitest.config.tsimport { defineConfig } from "vitest/config";import nola from "@nola-lang/vite";
export default defineConfig({ plugins: [nola()],});// not-checked — test/person.test.tsimport { 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");});NOLA_TEST=1 npx vitest runRecord once, replay forever
Section titled “Record once, replay forever”To test with recorded model responses, capture a ledger during a live run and replay it in CI:
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.
What to assert
Section titled “What to assert”- 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,attemptsandfingerprint. See Observability. - Use valid response shapes for successful cases, and deliberately invalid responses to exercise correction and error handling.
Next: Error handling