Test Framework Plugins
Zero-config integration for vitest and jest. Import useAimock, write tests
— the server lifecycle, env vars, and cleanup are handled automatically.
Vitest
import { useAimock } from "@copilotkit/aimock/vitest";
const mock = useAimock({ fixtures: "./fixtures" });
it("responds to hello", async () => {
// OPENAI_BASE_URL is already set
const res = await myApp.chat("hello");
expect(res).toBe("Hi there!");
});
Jest
import { useAimock } from "@copilotkit/aimock/jest";
const mock = useAimock({ fixtures: "./fixtures" });
it("responds to hello", async () => {
const res = await myApp.chat("hello");
expect(res).toBe("Hi there!");
});
Options
Both plugins accept the same UseAimockOptions object:
| Option | Type | Default | Description |
|---|---|---|---|
fixtures |
string |
— | Path to fixture file or directory |
patchEnv |
boolean |
true |
Auto-set OPENAI_BASE_URL and ANTHROPIC_BASE_URL |
port |
number |
0 (random) |
Port to listen on |
host |
string |
127.0.0.1 |
Host to bind to |
logLevel |
string |
silent |
Log verbosity |
fakesReport |
string |
"off" |
"off", "warn" or "fail": check each
test’s MCP fake report in afterEach.
"fail" fails the test, "warn" prints the report text
|
testIdRoot |
string |
working directory |
Jest only: the directory default test ids are relative to
(process.cwd() when useAimock is called)
|
The Handle
The getter function returned by useAimock() returns an
AimockFakesHandle: an AimockHandle (llm and
url) plus the MCP fakes helpers fakesFor() and
fakesReport(). Both types are exported from /vitest and
/jest:
| Property | Type | Description |
|---|---|---|
llm |
LLMock |
The LLMock instance — add fixtures programmatically |
url |
string |
The server URL (e.g., http://127.0.0.1:4010) |
fakesFor() |
function |
fakesFor(testId?, { context?, mount? }) returns
{ testId, mcpUrl, headers }: the MCP URL and headers that reach the
fakes for a test id. It also registers that test id for this test’s report.
See
below
|
fakesReport() |
function |
fakesReport(testId?, { context? }) resolves to the
MCP fake report for a test id
|
Programmatic fixture registration
const mock = useAimock();
it("custom fixture", async () => {
mock().llm.onMessage("custom", { content: "Custom response" });
// ...
});
Lifecycle
Both plugins register framework hooks to manage the server automatically:
-
beforeAll— starts the server, loads fixtures from thefixturespath, and patchesOPENAI_BASE_URL/ANTHROPIC_BASE_URLenvironment variables -
beforeEach— resets match counts (sequential fixture counters return to zero, but fixtures themselves are preserved) and marks every MCP fake entry as unused -
afterEach— withfakesReportset to"warn"or"fail", checks the test’s MCP fake report -
afterAll— stops the server and restores the original environment variables
MCP fakes
The fixtures path is loaded with LLMock.loadFixtureFile() or
loadFixtureDir(), so a file’s
mcpFakes load too, and aimock mounts
an MCPMock at each block’s mount path. A bad
mcpFakes block fails beforeAll with a
FixtureLoadError. Other load failures still only print a warning, as before.
Because beforeEach marks the fake entries as unused, each test starts with
its full list of answers.
With aimock-pytest, aimock.load_fixtures(path) posts a
file’s mcpFakes to the control API with its fixtures. A file may hold
only mcpFakes. When the server rejects the file with HTTP 400, the
requests.HTTPError message is
aimock rejected fixtures from <path>: <error>, followed by one
line for each item of details. When the file has mcpFakes and
the aimock server is a release without MCP fakes, load_fixtures raises
RuntimeError (aimock server too old for mcpFakes: ...) and adds
nothing from the file.
Fakes for a test: fakesFor()
mock().fakesFor() returns the test id, the MCP URL with
?testId=, and the matching X-Test-Id (and
X-AIMock-Context) headers. Give the URL or the headers to your MCP client.
mount picks the MCP mount (default /mcp). With no test id,
fakesFor() uses a default made from the running test:
-
Vitest:
<file> › <describe> › … › <test>.<file>is the test file relative to the Vitest project root, with/separators. -
Jest:
<file> › <describe and test names>, where the names are Jest’scurrentTestName(joined by single spaces) and<file>is relative totestIdRoot.
The default test id is used only by fakesFor() and
fakesReport(). It is not added to LLM or control requests, and an explicit
test id always wins. In Vitest, two tests that would get the same default (the same names
in one file), a concurrent test, and a call outside a test throw
AimockTestIdCollisionError; a retry of the same test reuses its id. Jest
cannot tell two tests with the same full name apart, so they share one default id: pass an
explicit test id for them. In test.concurrent, Jest can report another
test’s name, so pass an explicit test id there too.
fakesReport is not supported in concurrent tests. The report keeps one state
per useAimock(), so concurrent tests would clear and read each other’s
test ids. In Vitest, with fakesReport: "warn" or "fail", a
test.concurrent or describe.concurrent test fails in
beforeEach with “fakesReport is not supported in concurrent
tests”. Jest cannot tell the plugin that a test is concurrent, so it does not stop
you: keep fakesReport: "off" for a suite with test.concurrent,
or run its tests sequentially. With "off", concurrent tests can still use
fakesFor() with an explicit test id.
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { describe, expect, it } from "vitest";
import { useAimock } from "@copilotkit/aimock/vitest";
const mock = useAimock({ fixtures: "./fixtures", fakesReport: "fail" });
describe("weather", () => {
it("seattle", async () => {
// The default test id here is "test/weather.spec.ts › weather › seattle".
const { mcpUrl } = mock().fakesFor();
const client = new Client({ name: "weather-test", version: "1.0.0" });
await client.connect(new StreamableHTTPClientTransport(new URL(mcpUrl)));
const result = await client.callTool({
name: "get_weather",
arguments: { city: "Seattle" },
});
expect(result.content).toEqual([{ type: "text", text: "rain" }]);
await client.close();
});
});
{
"mcpFakes": {
"scope": { "testId": "test/weather.spec.ts › weather › seattle" },
"undeclaredTools": "deny",
"tools": [
{
"name": "get_weather",
"calls": [{ "args": { "city": "Seattle" }, "result": "rain" }]
}
]
}
}
Fake reports: fakesReport
A test can pass while its MCP fakes went wrong: many agent frameworks catch a tool error
and give it to the model, and nothing fails when a declared fake is never called. With
fakesReport: "fail", afterEach reads the
fake report and throws
AimockFakesReportError when it is not ok: a fake error (such as
MCP_FAKE_MISMATCH), an unused entry, or lost state. The message lists the
failures first, then the unused entry ids. "warn" prints the same text with
console.warn, and "off" (the default) checks nothing.
The check covers every test id registered with fakesFor() in the test, and
every test id (and context) that MCP requests to this server carried during the test, read
from the journal. A test with neither is not checked. If the journal is cleared during a
test, only the requests still in it are checked. mock().fakesReport() returns
the report itself.
Without Plugins (Manual)
For comparison, here is the equivalent manual setup. The plugins above handle all of this for you:
import { LLMock } from "@copilotkit/aimock";
let mock: LLMock;
beforeAll(async () => {
mock = new LLMock();
mock.onMessage("hello", { content: "Hi!" });
await mock.start();
process.env.OPENAI_BASE_URL = `${mock.url}/v1`;
});
afterAll(async () => {
await mock.stop();
delete process.env.OPENAI_BASE_URL;
});