Testing autonomous agents built on GPT-5 demands isolating the model’s reasoning from the side effects of its tools. Mocking tool calls in GPT-5 agent tests lets you assert on the agent’s decision logic without burning tokens or triggering real bank transfers. This guide walks through a reproducible pattern using Python, pytest, and the OpenAI SDK.
Step 1: Define a minimal agent loop with tool calling
Start with a plain function that runs the standard ReAct-style loop. It sends messages to the model, checks for tool_calls, executes them, and feeds results back until the model returns text.
from openai import OpenAI
client = OpenAI()
TOOLS = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current temperature for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}
]
def run_agent(messages, max_turns=5):
for _ in range(max_turns):
resp = client.chat.completions.create(
model="gpt-5",
messages=messages,
tools=TOOLS,
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content
for call in msg.tool_calls:
# dispatch to real tool
result = dispatch_tool(call.function.name, call.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result,
})
return "MAX_TURNS_EXCEEDED"
The dispatch_tool function is the seam we will mock later.
Step 2: Separate tool dispatch from business logic
Put tool execution behind a small registry. This makes it trivial to swap implementations in tests.
import json
def dispatch_tool(name, args_json):
args = json.loads(args_json)
if name == "get_weather":
return _get_weather(args["city"])
raise ValueError(f"Unknown tool: {name}")
def _get_weather(city):
# real implementation hits an API
raise NotImplementedError("live API call")
By isolating _get_weather, you avoid patching deep in the stack. Mocking tool calls in GPT-5 agent tests becomes a two-surface problem: the model response and the tool side effect.
Step 3: Mock the GPT-5 response to force specific tool calls
Use unittest.mock to replace client.chat.completions.create. Return a fake response object that mimics the SDK’s structure. Build a helper that yields a tool-call message on the first call and a final answer on the second.
from types import SimpleNamespace
from unittest.mock import patch
def tool_call_response():
return SimpleNamespace(
choices=[SimpleNamespace(
message=SimpleNamespace(
content=None,
tool_calls=[SimpleNamespace(
id="call_1",
type="function",
function=SimpleNamespace(
name="get_weather",
arguments='{"city":"SF"}'
)
)]
)
)]
)
def final_response():
return SimpleNamespace(
choices=[SimpleNamespace(
message=SimpleNamespace(
content="It is 60F in SF.",
tool_calls=None
)
)]
)
Now patch the client in your test:
@patch("__main__.client.chat.completions.create")
def test_agent_requests_weather(mock_create):
mock_create.side_effect = [tool_call_response(), final_response()]
out = run_agent([{"role": "user", "content": "Weather in SF?"}])
assert out == "It is 60F in SF."
This proves the agent loops correctly when the model emits a tool call.
Step 4: Mock the tool implementations
Patch dispatch_tool to return canned data. This removes network dependencies and lets you test agent logic against known tool outputs.
@patch("__main__.dispatch_tool", return_value='{"temp_f": 60}')
@patch("__main__.client.chat.completions.create")
def test_agent_uses_tool_result(mock_create, mock_dispatch):
mock_create.side_effect = [tool_call_response(), final_response()]
out = run_agent([{"role": "user", "content": "Weather in SF?"}])
mock_dispatch.assert_called_once_with("get_weather", '{"city":"SF"}')
assert "60F" in out
If your agent parses the tool JSON and reformats it, you can assert on the exact string the model receives in the second turn by capturing messages passed to create.
Step 5: Write a deterministic test harness
Combine both mocks into a reusable fixture. Below is a full pytest module that runs end to end.
import json
import pytest
from types import SimpleNamespace
from unittest.mock import patch
from my_agent import run_agent, dispatch_tool, client, TOOLS
def make_tool_call(name, args, call_id="c1"):
return SimpleNamespace(
id=call_id,
type="function",
function=SimpleNamespace(name=name, arguments=json.dumps(args))
)
def resp_with(message):
return SimpleNamespace(choices=[SimpleNamespace(message=message)])
@pytest.fixture
def mocked_run():
with patch.object(client.chat.completions, "create") as mock_create, \
patch("my_agent.dispatch_tool") as mock_dispatch:
yield mock_create, mock_dispatch
def test_multi_turn_agent(mocked_run):
mock_create, mock_dispatch = mocked_run
mock_dispatch.return_value = '{"temp_f": 72}'
# turn 1: model calls tool
# turn 2: model answers
mock_create.side_effect = [
resp_with(SimpleNamespace(content=None, tool_calls=[make_tool_call("get_weather", {"city": "LA"})])),
resp_with(SimpleNamespace(content="LA is 72F.", tool_calls=None)),
]
result = run_agent([{"role": "user", "content": "LA weather?"}])
assert result == "LA is 72F."
mock_dispatch.assert_called_with("get_weather", '{"city": "LA"}')
Run with pytest -q. A green check and the assertion output confirm the agent respects the mocked tool contract.
Step 6: Verify success and catch regressions
Success means the test passes without any outbound HTTP requests. Confirm by enabling assert_no_requests via responses or by setting OPENAI_API_KEY to a dummy and watching for no connection attempts. Add a second test that forces a tool error:
def test_tool_error_propagates(mocked_run):
mock_create, mock_dispatch = mocked_run
mock_dispatch.side_effect = RuntimeError("API down")
mock_create.side_effect = [
resp_with(SimpleNamespace(content=None, tool_calls=[make_tool_call("get_weather", {"city": "NY"})])),
resp_with(SimpleNamespace(content="Could not fetch weather.", tool_calls=None)),
]
result = run_agent([{"role": "user", "content": "NY weather?"}])
assert "Could not fetch" in result
If your agent retries or switches tools, encode that in the side_effect list. Mocking tool calls in GPT-5 agent tests gives you a fast feedback loop—typical suite runtime drops from seconds per call to microseconds.
Step 7: Handle parallel tool calls and streaming
GPT-5 supports multiple tool_calls in one message. Your mock must return a list:
tool_calls=[
make_tool_call("get_weather", {"city": "SF"}, "c1"),
make_tool_call("get_weather", {"city": "NY"}, "c2"),
]
Loop over them exactly as the agent does. For streaming, patch client.chat.completions.create_stream and yield delta objects; the same dispatch seam applies.
Gateway and schema compatibility
If you route through an OpenAI-compatible gateway such as n4n.ai, the response schema is identical, so the mocks above work unchanged at the client boundary. The gateway’s automatic fallback and cache-control forwarding happen server-side and do not affect unit tests that patch the SDK.
Closing checklist
- Keep
dispatch_toolas the single mock point for side effects. - Patch
client.chat.completions.createto simulate model decisions. - Assert on the
messageslist to verify the agent fed correct tool results back. - Run
pytestwith network isolation to guarantee no live calls.
Following these steps makes mocking tool calls in GPT-5 agent tests a routine part of your CI, not a brittle afterthought.