1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
17 small wins to finish your pathNext lesson →
Testing with node:test and mock models
Unit tests replace the model with MockLanguageModelV4 answers you choose, then check what your code does with them and what it sent to the model.
import { test } from "node:test";
import assert from "node:assert/strict";
import { generateText, Output } from "ai";
import { MockLanguageModelV4 } from "ai/test";
import { Ticket } from "./ticket.ts";
const usage = {
inputTokens: { total: 1, noCache: 1, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 1, text: 1, reasoning: undefined },
};
const answer = (text: string) => new MockLanguageModelV4({
doGenerate: async () => ({ content: [{ type: "text", text }], finishReason: { unified: "stop", raw: undefined }, usage, warnings: [] }),
});
test("a valid ticket is parsed", async () => {
const { output } = await generateText({ model: answer('{"category":"billing","priority":5}'), output: Output.object({ schema: Ticket }), prompt: "x" });
assert.deepEqual(output, { category: "billing", priority: 5 });
});
test("a priority of 9 is rejected", async () => {
const run = generateText({ model: answer('{"category":"billing","priority":9}'), output: Output.object({ schema: Ticket }), prompt: "x" });
await assert.rejects(run, { name: "AI_NoObjectGeneratedError" });
});
test("the system prompt reaches the model", async () => {
const model = answer("ok");
await generateText({ model, system: "Be kind.", prompt: "Hi" });
assert.deepEqual(model.doGenerateCalls[0].prompt[0], { role: "system", content: "Be kind." });
});npx tsx --test ticket.test.tsNode's built-in test runner, run through tsx with --test. answer builds a mock that returns fixed text, so each test controls the model exactly:
- A valid answer parses into the typed object.
- An invalid one, priority 9, rejects with
AI_NoObjectGeneratedError: your schema's limits are tested without a model misbehaving on cue. doGenerateCallsproves the system prompt was sent first.
The same pattern works with Vitest or Jest. simulateReadableStream and doStream test streaming code, as shop-model.ts does.
Try it yourself
- Test that a
categoryofrefundsis rejected. - Test the
tools.tstool'sexecutedirectly. - Use
mockValuesfromai/testto answer differently on each call.
Every expert started right here.