Skip to main content

Testing Agents

The Agents package ships with first-class testing primitives that let you exercise agent behavior without making real LLM calls. By combining FakeAgentDriver with FakeTool, you can script deterministic scenarios, assert on individual steps, and verify that your agent’s tool-calling logic, error handling, and multi-step loops behave exactly as expected.

FakeAgentDriver

FakeAgentDriver replaces a real driver (such as ToolCallingDriver or ReActDriver) with a scripted sequence of steps. Each step defines what the “LLM” would return — a final response, a tool call, an intermediate message, or an error. The driver advances through the script one step per loop iteration, giving you full control over the agent’s behavior.

Creating a Driver from Simple Responses

The simplest way to create a fake driver is from one or more string responses. Each string becomes a FinalResponse step, meaning the agent loop will stop after consuming it:
When all scripted steps are exhausted, the driver replays the last step indefinitely. This is useful for agents that may be executed multiple times against the same driver instance.

Creating a Driver from Scenario Steps

For more sophisticated tests — especially those involving tool calls — use ScenarioStep objects directly:
In this example, the first iteration produces a tool call step (the loop continues), and the second iteration produces a final response (the loop stops).

ScenarioStep Types

ScenarioStep is a readonly value object that describes what a single agent loop iteration should produce. Four factory methods cover the common cases:

ScenarioStep::final()

Produces a FinalResponse step. The agent loop recognizes this as a terminal response and stops iterating:

ScenarioStep::tool()

Produces a ToolExecution step type without attaching any tool calls. This is useful for simulating intermediate LLM responses that signal the loop should continue (because the step type is ToolExecution), but where no actual tool invocation is needed:
Note: Because no tool calls are attached, this step will not trigger the ToolExecutor. If you need actual tool execution, use ScenarioStep::toolCall() instead.

ScenarioStep::error()

Produces an Error step. The step is created with a RuntimeException attached, which the agent loop treats as a failure:

ScenarioStep::toolCall()

Produces a ToolExecution step with a tool call attached. This is the most powerful step type — it simulates the LLM requesting a specific tool and optionally executes it through the ToolExecutor:
When executeTools is true, the tool call is forwarded to the ToolExecutor, which resolves the tool from the Tools collection and invokes it. Set it to false to skip execution entirely — useful when you only need to verify that the correct tool call was produced.

Custom Usage Tracking

All step factories accept an optional InferenceUsage parameter for testing token-budget guards or usage reporting:

FakeTool

FakeTool creates tool stubs that implement both ToolInterface and CanDescribeTool. They can be registered in a Tools collection and will be resolved by the ToolExecutor when a matching tool call arrives.

Fixed Return Value

The simplest mock returns the same value regardless of the arguments passed:
The three arguments are: tool name, description (used in the tool schema), and the fixed return value.

Custom Logic

For more realistic stubs, pass a callable that receives the tool’s arguments and returns a result:
The callable is invoked with the same arguments the LLM would pass via the tool call. The return value is wrapped in a Result::from() automatically.

Custom Schema and Metadata

When you need the fake to advertise a specific JSON Schema (for example, to test schema validation), pass the schema parameter:

Full Test Example

The following Pest test demonstrates the complete pattern: create a FakeTool, script a FakeAgentDriver with a tool-call step followed by a final-response step, wire them into an AgentLoop, and assert on the result:

Using iterate() for Step-Level Testing

The AgentLoop::iterate() method returns a generator that yields the AgentState after each completed step. This gives you fine-grained visibility into intermediate states — useful for asserting on tool execution ordering, intermediate messages, or guard behavior:
Tip: The final state yielded by iterate() includes the withExecutionCompleted() transition, so you can also assert on ExecutionStatus and total usage.

Testing Error Handling

You can verify that your agent handles errors gracefully by scripting error steps:

Testing Subagent Scenarios

FakeAgentDriver supports child steps for subagent testing. When the driver is cloned for a subagent (via withLLMProvider() or withLLMConfig()), it uses the child steps instead of the parent steps:
If no child steps are provided, subagent drivers default to a single ScenarioStep::final('ok').

Testing with Events

You can attach event listeners to the AgentLoop to capture events emitted during execution. This is useful for verifying that specific lifecycle events fire at the right time:

Summary