agenttest 🧪
Part of the Agent OS suite — kernel · network · memory · policy · audit · testing
The testing framework for AI agents.
Think pytest — but for agents.
Software engineering has unit tests, integration tests, and CI/CD pipelines. But when you build an AI agent, how do you test it? How do you know:
- Did the agent call the right tools in the right order?
- Is the output actually correct — not just non-empty?
- Will it still work correctly if you run it 10 times in a row?
- Did it reason through the problem, or just guess?
agenttest fills this gap. It's a lightweight, zero-dependency testing framework that gives agent engineers the primitives to write real tests for real agent behavior.
Installation
pip install agenttest
Or install from source:
git clone https://github.com/cdzzy/agenttest
cd agenttest
pip install -e .
Quick Start
from agenttest import AgentTestCase, AgentTestSuite, AgentTestRunner
class MyAgentTests(AgentTestCase):
agent = my_agent # your agent callable
def test_uses_search(self):
run = self.invoke("What is the capital of France?")
self.assert_tool_called(run, "web_search")
self.assert_output_contains(run, "Paris")
def test_no_error(self):
run = self.invoke("Tell me a joke")
self.assert_no_error(run)
self.assert_output_length(run, min_chars=10)
suite = AgentTestSuite("My Agent")
suite.add_case(MyAgentTests)
runner = AgentTestRunner()
runner.run_suite(suite)
Output:
============================================================
AgentTest — My Agent
2 test(s) collected
============================================================
✅ MyAgentTests.test_uses_search (142ms)
✅ MyAgentTests.test_no_error (98ms)
============================================================
✅ 2 passed
⏱ Total time: 240ms
============================================================
Core Concepts
1. AgentRun — The unit of inspection
Every time you invoke an agent, you get back an AgentRun:
run = self.invoke("Search for Python tutorials")
run.output # str: what the agent said
run.tool_calls # list: all tool invocations [{name, input, output}, ...]
run.tool_names # list[str]: just the names, in call order
run.reasoning_steps # list[str]: the agent's thought process
run.duration_ms # float: how long it took
run.tokens_used # int: token count (if reported)
run.error # Exception | None
Your agent just needs to return a dict with these keys, or a plain string:
def my_agent(input_text: str) -> dict:
# ... run your agent ...
return {
"output": "The capital of France is Paris.",
"tool_calls": [{"name": "web_search", "input": input_text, "output": "..."}],
"reasoning_steps": ["I need to look this up.", "Found the answer."],
}
2. Behavior Assertions — Did the agent do the right things?
from agenttest.assertions.behavior import (
assert_tool_called,
assert_tool_sequence,
assert_no_tool_called,
assert_tool_called_before,
assert_max_tool_calls,
)
# Was a tool called at all?
assert_tool_called(run, "web_search")
# Was it called exactly twice?
assert_tool_called(run, "web_search", times=2)
# Were tools called in the right order?
assert_tool_sequence(run, ["web_search", "summarize"])
# Strict mode: exactly these tools, exactly this order
assert_tool_sequence(run, ["search", "format"], strict=True)
# Was a dangerous tool NOT called?
assert_no_tool_called(run, "delete_database")
# Did search happen before summarization?
assert_tool_called_before(run, "web_search", "summarize")
# Did the agent stay within a reasonable tool budget?
assert_max_tool_calls(run, max_calls=10)
3. Output Assertions — Did the agent say the right thing?
from agenttest.assertions.output import (
assert_output_contains,
assert_output_not_contains,
assert_output_matches,
assert_output_length,
assert_output_contains_all,
assert_output_json,
assert_output_sentiment,
)
assert_output_contains(run, "Paris")
assert_output_not_contains(run, "I don't know")
assert_output_matches(run, r"\d{4}-\d{2}-\d{2}") # ISO date
assert_output_length(run, min_chars=50, max_chars=2000)
assert_output_contains_all(run, ["name", "age", "email"])
assert_output_json(run) # valid JSON
assert_output_json_schema(run, {"type": "object", "required": ["name", "email"]}) # schema validation
assert_output_sentiment(run, "positive")
4. Trace Assertions — Did the agent think correctly?
from agenttest.assertions.trace import (
assert_reasoning_step,
assert_step_count,
assert_no_reasoning_loops,
assert_tool_reasoning_alignment,
)
# Does the reasoning mention the right thing?
assert_reasoning_step(run, "search for")
# Did the agent use a reasonable number of thoughts?
assert_step_count(run, min_steps=1, max_steps=5)
# Is the agent stuck in a loop?
assert_no_reasoning_loops(run)
# Did the agent justify its tool calls?
assert_tool_reasoning_alignment(run)
5. Stability Testing — Is the agent reliable?
from agenttest.assertions.stability import StabilityAssertion
stability = StabilityAssertion(my_agent, runs=10, pass_rate=1.0)
# Run 10 times — should always contain "Paris"
stability.assert_consistent_output(
"What is the capital of France?",
check_fn=lambda run: "Paris" in run.output,
)
# Output should be identical every run (temperature=0)
stability.assert_output_deterministic("What is 6 * 7?")
# Same tools should be called every run
stability.assert_tool_usage_consistent("Search for Python docs")
# Just measure the pass rate
rate = stability.get_pass_rate("...", check_fn=lambda r: "answer" in r.output)
print(f"Pass rate: {rate*100:.0f}%")
6. Mock Tools — Test without side effects
from agenttest import MockToolkit
toolkit = MockToolkit()
toolkit.add("web_search", returns="Paris is the capital of France.")
toolkit.add("send_email", returns="Email sent.")
toolkit.add("calculator", side_effect=lambda expr: str(eval(expr)))
# Inject into your agent
agent = MyAgent(tools=toolkit.as_dict())
agent("What is the capital of France?")
# Assert on tool usage
toolkit["web_search"].assert_called()
toolkit["send_email"].assert_not_called() # shouldn't email anyone
assert toolkit["web_search"].call_count == 1
7. Scenario Builder — Batch test from data
from agenttest import ScenarioBuilder
scenarios = (
ScenarioBuilder()
.add("Capital query")
.input("What is the capital of France?")
.expect_tools("web_search")
.expect_output_contains("Paris")
.done()
.add("Math problem")
.input("What is 6 * 7?")
.expect_tools("calculator")
.expect_output_contains("42")
.done()
.build()
)
# Load from files
scenarios = ScenarioBuilder.from_jsonl("test_cases.jsonl")
scenarios = ScenarioBuilder.from_csv("test_cases.csv")
Decorator Style (pytest-like)
from agenttest import agent_test
@agent_test(tags=["smoke"])
def test_basic(agent):
run = agent("Hello!")
assert run.output
@agent_test(repeat=5, tags=["stability"]) # run 5 times
def test_stable(agent):
run = agent("What is 2+2?")
assert "4" in run.output
Agent Return Format
agenttest works with any agent that returns a dict or string:
# Dict format (recommended — enables all assertions)
def my_agent(input_text: str) -> dict:
return {
"output": "...", # required
"tool_calls": [...], # for behavior assertions
"reasoning_steps": [...], # for trace assertions
"usage": {"total_tokens": 0}, # optional
}
# String format (basic output assertions only)
def simple_agent(input_text: str) -> str:
return "Hello!"
Integrations
LangChain / LangGraph
from langchain_core.messages import HumanMessage
def wrap_langgraph(graph):
def agent(input_text: str) -> dict:
result = graph.invoke({"messages": [HumanMessage(content=input_text)]})
return {
"output": result["messages"][-1].content,
"tool_calls": extract_tool_calls(result),
"reasoning_steps": extract_thoughts(result),
}
return agent
AutoGen
def wrap_autogen(team):
def agent(input_text: str) -> dict:
result = team.run_sync(task=input_text)
return {
"output": result.messages[-1].content,
"tool_calls": [m for m in result.messages if hasattr(m, "tool_calls")],
}
return agent
Examples
examples/
test_basic_agent.py # Getting started
test_stability.py # Stability / reliability testing
test_langgraph_agent.py # LangGraph integration
test_scenarios.py # Scenario-driven batch testing
Run all examples:
python examples/test_basic_agent.py
python examples/test_stability.py
python examples/test_langgraph_agent.py
python examples/test_scenarios.py
Roadmap
-
agenttestCLI (agenttest edit --ui,agenttest --version) ✅ (agenttest/cli.py, v0.2.0) - Async agent support (
async def) ✅ (agenttest/core/runner.py, v0.2.0) - Behavior snapshots (regression testing) ✅ (agenttest/snapshots.py, v0.2.0)
- Benchmarking (multi-model comparison) ✅ (agenttest/benchmark.py, v0.2.0)
- Chaos engineering (resilience testing) ✅ (agenttest/chaos.py, v0.2.0)
- Flakiness detection ✅ (agenttest/flakiness.py, v0.2.0)
- Suite run history (pass-rate regression) ✅ (agenttest/core/history.py, v0.2.0)
- MCP server testing ✅ (agenttest/mcp_tools.py, v0.2.0)
- Reasoning model assertions ✅ (agenttest/assertions/trace.py, v0.2.0)
- Visual test editor ✅ (agenttest/ui/editor.html, v0.2.0)
- GitHub Action ✅ (action.yml, v0.2.0)
- HTML/JSON test reports ✅ (agenttest/report.py, v0.3.0)
- Pytest plugin (
pytest11entry point — run@agent_testfunctions under vanilla pytest with theagentfixture) ✅ (v0.4.0) -
LLM-as-judge assertions✅ (examples/test_llm_judge.py) -
GitHub Actions template✅ (.github/workflows/ci.yml) -
YAML test configuration✅ (load tests from YAML files, inspired by PraisonAI) - JSON Schema assertion (validate structured agent output against JSON Schema draft-07, examples/test_json_schema.py)
Contributing
PRs welcome. The goal is to keep agenttest simple, zero-dependency (for the core), and genuinely useful for production agent engineering.
git clone https://github.com/cdzzy/agenttest
cd agenttest
pip install -e ".[dev]"
python examples/test_basic_agent.py
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cdzzy_agenttest-0.4.0.tar.gz.
File metadata
- Download URL: cdzzy_agenttest-0.4.0.tar.gz
- Upload date:
- Size: 55.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26abdcec34d83522e04f961c374330eb4e6be2ac05f75d727a960d821367cc6e
|
|
| MD5 |
8db5448f8b168b5fdb8c6ac6822fe68b
|
|
| BLAKE2b-256 |
026aba16a7655ee41fb2e241da9d337d8b24e88d698849265562f3aa450ac12e
|
File details
Details for the file cdzzy_agenttest-0.4.0-py3-none-any.whl.
File metadata
- Download URL: cdzzy_agenttest-0.4.0-py3-none-any.whl
- Upload date:
- Size: 56.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5264851ea8d99f4afe60fc918884f493f2e472b18ca73c52fc31be8107ffbb7c
|
|
| MD5 |
c5faa15a1ca0f90b0364733712872127
|
|
| BLAKE2b-256 |
d6b68b484978d01094e2a4b40aaaa380f9ac253bf1877ef62a7205974cfc32d9
|