Data & Analysis

Mastering AI Agent Testing in Development: Our Custom Framework and Best Practices

Discover how we tackle the complexities of testing AI agents with a robust, open-source framework built on Pytest. Layered testing, observability, and CI/CD integration ensure reliable deployments.

J

Jennifer Yu

Workflow Automation Specialist

December 30, 2025 min read
Share:

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

AspectTraditional PytestOur Agent Framework
LLM HandlingManual mocksAuto-provisioned fixtures with retries
Graph TestingCustom setupsNative LangGraph support
ObservabilityNoneLangSmith integration
ScalabilityLimitedParallel 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_tool with 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

  1. Start Small: Unit test tools first, then integrate.
  2. Mock Aggressively: Only live test critical paths.
  3. Monitor Drift: Retrain expectations periodically as models evolve.
  4. 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>
The #1 Newsletter in AI

Stay ahead of the AI curve

The most important updates, news, and content — delivered in one weekly newsletter.

No spam. Unsubscribe anytime. Privacy policy

ai-agents
testing-frameworks
langchain
pytest
langsmith
ci-cd
llm-testing
J

About Jennifer Yu

Workflow Automation Specialist

Jennifer covers workflow strategy, no-code platforms, and clear implementation guidance for teams adopting automation.

Comments (0)