Testing AI agents is fundamentally different from testing deterministic software. The same input can produce different outputs. Tools have side effects. And the "correctness" of a response is often subjective.
That doesn't mean you skip testing. It means you need a different strategy.
Why Traditional Tests Break Down
A unit test asserts that add(2, 3) === 5. Every run, same result. For an AI agent, the same prompt might produce slightly different tool call sequences, different phrasings, or — under model updates — different reasoning paths altogether.
You can't pin-assert LLM outputs. You need to assert on properties instead.
Layer 1: Tool Tests (Deterministic)
Your agent's tools — database queries, API calls, file operations — are regular code. Test them like regular code.
describe('searchProducts tool', () => {
it('returns results matching query', async () => {
const results = await searchProducts({ query: 'laptop', limit: 5 })
expect(results).toHaveLength(5)
expect(results[0]).toHaveProperty('id')
expect(results[0]).toHaveProperty('price')
})
})This layer is fast, cheap, and gives you high confidence that your agent's capabilities are wired up correctly.
Layer 2: Evals (LLM-as-Judge)
For the agent's reasoning and output quality, use evals — structured test cases where another LLM grades the output.
const eval = {
input: "Find me the cheapest laptop under $800 with at least 16GB RAM",
criteria: [
"Agent calls searchProducts at least once",
"Response includes at least one product recommendation",
"Recommended product matches the constraints (price < $800, RAM >= 16GB)",
"Response explains why the recommendation fits the requirements",
]
}Run these evals on every model update, every significant prompt change, and before every production deploy.
Layer 3: Integration Tests with Mocked Tools
Test the full agent loop with real LLM calls but mocked external tools. This catches prompt issues, tool selection bugs, and loop logic failures without hitting production APIs.
const mockTools = {
searchProducts: jest.fn().mockResolvedValue(mockProductData),
addToCart: jest.fn().mockResolvedValue({ success: true }),
}
const agent = new ShoppingAgent({ tools: mockTools })
const result = await agent.run("Add the cheapest red sneaker to my cart")
expect(mockTools.searchProducts).toHaveBeenCalledWith(
expect.objectContaining({ category: 'sneakers' })
)
expect(mockTools.addToCart).toHaveBeenCalledOnce()Layer 4: Golden Path E2E Tests
A small set of end-to-end tests that run the full agent against real tools in a staging environment. These are slow and expensive — run them nightly, not on every commit.
Cover your core happy paths and your highest-risk failure modes.
Handling Non-Determinism
Three techniques for making evals repeatable:
- Fix temperature to 0 during testing — eliminates sampling variance
- Seed your model calls where the API supports it
- Assert on structure, not exact text — check that key facts appear, not that sentences match word-for-word
Regression Testing After Model Updates
When Anthropic releases a new Claude model, your prompts may behave differently. Run your full eval suite before switching model versions in production. We gate every model upgrade behind a passing eval run.
The Testing Pyramid for Agents
┌──────────────┐
│ E2E (few) │
┌┴──────────────┴┐
│ Integration │
┌┴────────────────┴┐
│ Evals (medium) │
┌┴──────────────────┴┐
│ Tool unit tests │
└────────────────────┘Most of your tests should be at the bottom — fast, cheap, deterministic. Evals in the middle give you quality coverage. A thin layer of E2E at the top gives you production confidence.
Start with tool tests and one eval suite. Add layers as your system matures.
