Testing agents is harder than testing functions — the path to a correct answer matters, not just the answer. An agent that gets the right result via an accidental sequence of tool calls is not a reliable agent.
Replace real tools and LLMs with deterministic mocks in tests. Fast, free, and reproducible — test agent logic without actually calling APIs.
import pytest
from unittest.mock import MagicMock, patch
from langgraph.graph import StateGraph
# ── Mock the LLM ──────────────────────────────────────────────
def make_mock_llm(responses: list[str]):
"""Returns a mock LLM that cycles through pre-defined responses."""
mock = MagicMock()
mock.invoke.side_effect = [
MagicMock(content=r, tool_calls=[]) for r in responses
]
return mock
# ── Mock tools ────────────────────────────────────────────────
def mock_search_tool(query: str) -> str:
"""Deterministic mock — always returns the same search result."""
return "Search result for: " + query
# ── Test agent behavior ───────────────────────────────────────
def test_agent_completes_without_tools():
"""Test that agent terminates when LLM doesn't call tools."""
mock_llm = make_mock_llm(["Here is the final answer: Paris"])
with patch("myapp.agent.model", mock_llm):
result = run_agent("What is the capital of France?")
assert "Paris" in result["messages"][-1].content
assert mock_llm.invoke.call_count == 1 # Called exactly once
def test_agent_uses_search_tool():
"""Test that agent correctly calls search and incorporates result."""
tool_response = MagicMock(content="", tool_calls=[{
"id": "call_001",
"name": "web_search",
"args": {"query": "population of Tokyo"},
}])
final_response = MagicMock(content="Tokyo has 13.96 million people.", tool_calls=[])
mock_llm = make_mock_llm([])
mock_llm.invoke.side_effect = [tool_response, final_response]
with patch("myapp.agent.model", mock_llm), \
patch("myapp.agent.web_search", mock_search_tool):
result = run_agent("What is the population of Tokyo?")
assert mock_llm.invoke.call_count == 2
assert "13.96 million" in result["messages"][-1].content| Level | Tests | Count | Run on |
|---|---|---|---|
| Unit (mocked) | State transitions, routing logic, tool input validation | ~30 | Every commit |
| Integration (real tools) | Tool calls work, API responses handled correctly | ~10 | Every PR |
| E2E eval (real LLM) | Agent achieves goal on golden dataset | ~20 tasks | Before release |
| Adversarial | Prompt injection, hallucination, loop-breaking inputs | ~10 | Before release |
You are now ready for the capstone: building a multi-agent research team that uses LangGraph, CrewAI, human-in-the-loop approval, and a full test suite.