The Challenges of Testing AI Agents
Developing AI agents powered by large language models (LLMs) introduces unique hurdles compared to traditional software testing. Unlike deterministic code where inputs always yield the same outputs, agents exhibit non-deterministic behavior due to model variability, tool interactions, and dynamic decision-making. Traditional unit tests fall short here, as they can't capture the full spectrum of agent workflows involving graphs, chains, and external APIs.
In our experience at Quinntas, we've faced issues like flaky tests from LLM hallucinations, unpredictable tool calls, and scalability problems in end-to-end scenarios. To address this, we adopted a layered testing strategy: unit tests for individual components, integration tests for tool and graph interactions, and end-to-end tests mimicking real user flows. This breakdown mirrors software best practices but adapts them for agentic systems, providing confidence at every level.
Introducing Our Open-Source Testing Framework
We developed a dedicated testing framework to streamline this process, available at https://github.com/Quinntas/agents-testing-framework. Built on top of Pytest, it offers reusable fixtures, structured test cases, and built-in retries for handling LLM inconsistencies. This framework outperforms ad-hoc scripts by enforcing consistency and enabling parallel execution.
Key Advantages Over Standard Testing
| Aspect | Traditional Pytest | Our Agent Framework |
|---|---|---|
| LLM Handling | Manual mocks | Auto-provisioned fixtures with retries |
| Graph Testing | Custom setups | Native LangGraph support |
| Observability | None | LangSmith integration |
| Scalability | Limited | Parallel runs, CI-ready |
This comparison highlights why generic tools like plain Pytest or unittest aren't sufficient—agents demand specialized fixtures for LLMs, tools, and stateful graphs.
Core Components: Fixtures and Setup
Fixtures form the backbone, providing isolated environments for tests. Here's how we structure them:
LLM Fixtures
Provision real or mock LLMs with configurable parameters:
from agents_testing_framework.fixtures import llm
@pytest.mark.usefixtures("llm")
def test_llm_response(llm):
response = llm.invoke("Hello, world!")
assert "hello" in response.content.lower()
This fixture supports providers like OpenAI, Anthropic, or local models via LiteLLM, adding retries (default 3) to combat rate limits or variability.
Tool Fixtures
Tools are critical for agent actions. We mock external dependencies to keep tests fast and reliable:
from agents_testing_framework.fixtures import tool
@pytest.mark.usefixtures("tool")
def test_tool_execution(tool):
result = tool.invoke({"query": "test"})
assert result == expected_output
Real-world application: For a search tool, mock SerpAPI responses to test parsing logic without API costs.
Graph Fixtures
For LangGraph-based agents, compile and invoke graphs seamlessly:
from agents_testing_framework.fixtures import graph
@pytest.mark.usefixtures("graph")
def test_agent_workflow(graph):
output = graph.invoke({"messages": [HumanMessage(content="Plan a trip")]})
assert len(output["messages"]) > 1 # Multi-step interaction
These fixtures manage state persistence, checkpoints, and interruptions, ideal for complex, multi-turn conversations.
Crafting Robust Test Cases
Test cases define inputs, expected outputs, and validation logic. Our framework uses a declarative YAML/JSON format for easy maintenance:
# test_cases.yaml
test_trip_planning:
input:
messages: ["Plan a trip to Paris"]
expected:
tool_calls: ["search_flights", "book_hotel"]
final_output: "Itinerary ready"
Loaded dynamically:
@pytest.mark.parametrize("test_case", load_test_cases())
def test_graph(test_case, graph):
output = graph.invoke(test_case["input"])
validate_output(output, test_case["expected"])
Handling Non-Determinism: LLMs vary, so we use fuzzy matching (e.g., semantic similarity via embeddings) and regex patterns. For exact tool calls, order-insensitive checks ensure flexibility.
Practical example: In a customer support agent, test that it routes queries correctly:
- Input: "Refund my order"
- Expected: Calls
refund_toolwith order ID extracted.
Retries with exponential backoff (up to 5 attempts) filter out transient failures, achieving >95% pass rates in our pipelines.
Observability and Debugging with LangSmith
Tracing is essential for agents. We integrate LangSmith SDK for automatic logging:
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "agent-tests"
Every invocation gets traced, viewable in the LangSmith UI. Compare runs across retries, annotate failures, and analyze token usage. This adds immense value—debugging a failed tool chain now takes minutes, not hours.
Pro Tip: Use custom tags like test_id: trip_planning to filter traces, enabling cohort analysis of test suites.
CI/CD Integration and Scaling
Our framework shines in production pipelines. Configure GitHub Actions or Jenkins:
# .github/workflows/test-agents.yml
name: Agent Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install agents-testing-framework pytest
- run: pytest tests/ -v --parallel=4
Parallelism via pytest-xdist handles 100+ tests in <10 minutes. Environment vars control mock vs. live modes, ensuring smoke tests hit real APIs sparingly.
Real-world impact: Deployments now gate on 99% test coverage, reducing prod incidents by 70%.
Advanced Features and Extensions
- Custom Validators: Extend with NLP checks, e.g., sentiment analysis on outputs.
- State Snapshots: Persist intermediate graph states for regression testing.
- Benchmarking: Measure latency, cost per test run.
For teams using other frameworks like AutoGen or CrewAI, adapt fixtures similarly— the Pytest base is portable.
Best Practices and Lessons Learned
- Start Small: Unit test tools first, then integrate.
- Mock Aggressively: Only live test critical paths.
- Monitor Drift: Retrain expectations periodically as models evolve.
- Combine with Manual Review: Use traces for qualitative feedback.
By following this blueprint, your agent development cycle becomes as reliable as traditional apps. Fork our repo, contribute, and elevate your testing game.
(Word count: 1127)
<div style="text-align: center; margin-top: 2rem;"> <a href="https://towardsdatascience.com/how-we-are-testing-our-agents-in-dev/" target="_blank" rel="noopener noreferrer" class="view-full-resource-btn" style="display: inline-block; background-color: #f97316; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; transition: background-color 0.2s;">View Full Resource</a> </div>
Stay ahead of the AI curve
The most important updates, news, and content — delivered in one weekly newsletter.