Testing your agents
August 26, 2026 · View on GitHub
An agent's behavior is mostly your code — which tools it has, what its instructions say, what it does with a tool result. None of that needs a real model to test, and testing it against one gives you a suite that is slow, costs money, and fails for reasons unrelated to your change.
The seam is the agents.Model interface: fake the model, run everything else
for real. Your tools actually execute; what is faked is only the decision to
call them.
A scripted model
Model has two methods, and a test double only needs the one its tests reach —
agents.RunSync calls Respond, the streaming entry point calls
StreamResponse:
type scriptedModel struct {
responses []*agents.ModelResponse // one per turn
calls int
}
func (m *scriptedModel) Respond(ctx context.Context, req agents.ModelRequest) (*agents.ModelResponse, error) {
res := m.responses[m.calls]
m.calls++
return res, nil
}
func (m *scriptedModel) StreamResponse(ctx context.Context, req agents.ModelRequest) iter.Seq2[*agents.ResponseStreamEvent, error] {
panic("this test suite only runs RunSync")
}
A turn's ModelResponse holds Responses-API output items: an assistant message
ends the run with that text as final output; a function-call item makes the run
execute your tool for real and feed its return value to the next scripted turn.
So "call the tool, then answer" is a two-response script.
Point the run at it with ModelOptions.Override and no provider is ever
consulted:
res, err := agents.RunSync(context.Background(), newAgent(), "the answer?",
agents.RunOptions{Model: agents.ModelOptions{Override: model}})
Override replaces the model for every agent in the run, which is what you
usually want in a test — a handoff target gets the same scripted model without
being wired up separately. To script one agent only, set Agent.ModelImpl
instead.
Worth asserting beyond the final output: that the script was fully consumed (a
run that stopped earlier than the test assumed is exactly what a test should
catch), and — for order-sensitive tests — the kinds of res.NewItems in
sequence.
When not to fake the model
A fake model stands in for the model. It does not stand in for anything else, on purpose — a test that stubs out the run loop is testing the stub.
- Testing a tool in isolation? Call it directly. A
Tool's function is an ordinary Go function; you do not need a run to exercise it. - Testing against the real API? Use a real provider and mark the test so it can be skipped without a key. Keep those few and separate from the fast suite.
- Writing a
Modelimplementation? Run it throughmodelkit/conformancetest— the golden matrix every in-repo backend passes — plus tests against the provider's wire format. A scripted fake is a consumer of theModelinterface, not a conformance suite for it.
The worked version of this page is examples/testing — a scripted model, the agent's real tool, and a test that asserts both the answer and that the script was fully consumed. It needs no API key: go test ./examples/testing.