Mocking
Sage’s testing framework provides first-class mocking for both LLM calls and tool calls. This makes your tests deterministic, fast, and independent of external services.
Mocking LLM Calls
You can specify exactly what divine calls should return using mock divine.
Basic Mocking
Use mock divine -> value; to specify what the next divine call should return:
test "divine returns mocked value" {
mock divine -> "This is a mocked response";
let result: String = try divine("Summarise something");
assert_eq(result, "This is a mocked response");
}
The mock is consumed by the divine call — each mock is used exactly once.
Multiple Mocks
When your test makes multiple divine calls, queue up multiple mocks in order:
test "multiple divine calls" {
mock divine -> "First response";
mock divine -> "Second response";
mock divine -> "Third response";
let r1 = try divine("Query 1");
let r2 = try divine("Query 2");
let r3 = try divine("Query 3");
assert_eq(r1, "First response");
assert_eq(r2, "Second response");
assert_eq(r3, "Third response");
}
Mocks are consumed in FIFO order (first in, first out).
Mocking Structured Output
For typed divine calls, mock with the appropriate record structure:
record Summary {
text: String,
confidence: Float,
}
test "structured divine returns typed mock" {
mock divine -> Summary {
text: "Quantum computing is fast.",
confidence: 0.88
};
let summary: Summary = try divine("Summarise quantum computing");
assert_eq(summary.text, "Quantum computing is fast.");
assert_gt(summary.confidence, 0.8);
}
Mocking Failures
Use fail("message") to mock a divine failure:
test "agent handles divine failure" {
mock divine -> fail("rate limit exceeded");
let handle = summon ResilientResearcher { topic: "test" };
let result = await handle;
// Agent's fallback behaviour
assert_eq(result, "unavailable");
}
This is essential for testing error handling paths.
Testing Agents with Mocks
When testing agents that use divine, mocks are consumed by the agent’s divine calls:
agent Researcher {
topic: String
on start {
let summary = try divine("Research: {self.topic}");
yield(summary);
}
on error(e) {
yield("Research failed");
}
}
test "researcher emits summary" {
mock divine -> "Quantum computing uses qubits.";
let result = await summon Researcher { topic: "quantum" };
assert_eq(result, "Quantum computing uses qubits.");
}
Testing Multi-Agent Systems
For agents that summon other agents, each agent’s divine calls consume mocks in execution order:
test "coordinator gets results from two researchers" {
mock divine -> "Summary about AI";
mock divine -> "Summary about robots";
let c = summon Coordinator {
topics: ["AI", "robots"]
};
let results = await c;
assert_contains(results, "AI");
assert_contains(results, "robots");
}
Mock Queue Exhaustion
If a divine call is made without an available mock, the test fails with error code E054:
Error: divine called with no mock available (E054)
Always provide enough mocks for all divine calls in your test.
Mocking Tool Calls
Just like LLM calls, you can mock tool calls (Http, Fs, etc.) to avoid real network or filesystem operations in tests.
Basic Tool Mocking
Use mock tool ToolName.method -> value; to specify what a tool call should return:
test "http get returns mocked response" {
mock tool Http.get -> HttpResponse {
status: 200,
body: "Hello, World!",
headers: {}
};
let response = try Http.get("https://example.com");
assert_eq(response.status, 200);
assert_eq(response.body, "Hello, World!");
}
Mocking Tool Failures
Use fail("message") to mock a tool failure:
test "handles network error gracefully" {
mock tool Http.get -> fail("connection timeout");
let result = catch Http.get("https://example.com");
assert_true(result.is_err());
}
Multiple Tool Mocks
Like mock divine, tool mocks are consumed in FIFO order:
test "multiple http calls" {
mock tool Http.get -> HttpResponse { status: 200, body: "first", headers: {} };
mock tool Http.get -> HttpResponse { status: 200, body: "second", headers: {} };
let r1 = try Http.get("https://api.example.com/1");
let r2 = try Http.get("https://api.example.com/2");
assert_eq(r1.body, "first");
assert_eq(r2.body, "second");
}
Mocking Different Tools
You can mock different tools in the same test:
test "agent uses multiple tools" {
mock tool Http.get -> HttpResponse { status: 200, body: "data", headers: {} };
mock tool Fs.read -> "config content";
mock divine -> "processed result";
let result = await summon DataProcessor {};
assert_eq(result, "processed result");
}
Testing Agents with Tool Mocks
When testing agents that use tools, mocks are consumed by the agent’s tool calls:
agent Fetcher {
url: String
use Http
on start {
let response = try Http.get(self.url);
yield(response.body);
}
}
test "fetcher returns body" {
mock tool Http.get -> HttpResponse {
status: 200,
body: "fetched content",
headers: {}
};
let result = await summon Fetcher { url: "https://example.com" };
assert_eq(result, "fetched content");
}
Best Practices
- One assertion per test — easier to identify failures
- Descriptive mock values — make it clear what’s being tested
- Test error paths — use
fail()to test error handling - Keep mocks simple — avoid complex JSON in mocks when possible
- Mock all external calls — both
divineand tool calls should be mocked for deterministic tests