n4nAI

How to mock tool calls when testing GPT-5 agents

Learn practical techniques for mocking tool calls in GPT-5 agent tests so you can unit test agent logic without hitting live APIs or external services.

n4n Team2 min read546 words

Audio narration

Coming soon — every post will get a voice note here.

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_tool as the single mock point for side effects.
  • Patch client.chat.completions.create to simulate model decisions.
  • Assert on the messages list to verify the agent fed correct tool results back.
  • Run pytest with 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.

Tagstool-callinggpt-5agent-testingmocking

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All testing ai agents & tool calling posts →