World’s largest virtual agentic engineering & quality conference
A practical guide to testing CrewAI crews: what to unit test without an LLM, how task guardrails enforce output contracts, how to assert on the event bus, what crewai test really measures, and how to validate a live crew.

Samyak Goyal
Author

Anubhav Singhmaar
Reviewer
Last Updated on: August 19, 2026
The single biggest frustration developers report with AI tooling is not that it fails outright. In the Stack Overflow Developer Survey 2025, 66% of developers named "AI solutions that are almost right, but not quite" as their top frustration, and the same survey found more developers actively distrust AI accuracy (46%) than trust it (33%). A CrewAI crew is a machine for producing almost-right output at scale.
That is what makes CrewAI testing awkward. The crew runs, every task completes, no exception is raised, and the report it hands back is subtly wrong. This guide covers what you can actually assert on, using the hooks CrewAI already ships. Every code example here was run against crewai 1.15.16 on Python 3.13, and the console output is real.
TL;DR
CrewAI is a role-based multi-agent framework. Its CrewAI GitHub repository shows more than 57,000 stars and requires Python 3.10 or later. You give each agent a role, a goal, and a backstory, hand the crew a list of tasks, and call kickoff().
Under the default sequential process, tasks execute one after another and each task inherits the context of the one before it. That is the design that makes crews easy to assemble, and it is also the design that lets one bad output travel.
That design creates four failure modes a green test suite will not catch:
None of these raise an exception, which is why assertEqual on the final string is the wrong instrument. Run the same crew twice with an identical prompt and you get two different strings, both acceptable. The useful question is not whether the output matches a fixture, but whether the crew did the right work and respected its contracts. The wider version of that problem is covered in our guide to multi agent testing.
Most guides to testing agents reach for an external evaluation tool on the first page. CrewAI already ships three testing surfaces of its own, and together with plain pytest they cover the layers where a bug is cheapest to catch.
| Hook | What it asserts on | Needs an LLM call? | Verdict shape |
|---|---|---|---|
| Plain pytest on tools | Your own Python: tool bodies, parsers, guardrail functions, listeners | No | Hard pass or fail |
| Task guardrails | The shape and content of one task output before it moves downstream | Only for string guardrails | Pass, or retry with feedback |
| Event bus listeners | Which tools ran, in what order, and which tasks completed or failed | Only for a full kickoff | Hard pass or fail on behavior |
| crewai test | Per-task output quality across repeated runs | Yes, OpenAI only | Score distribution, 1 to 10 |
Read the verdict column carefully, because it decides where each hook belongs. The first three produce a binary answer, so they can gate a pull request. crewai test returns a distribution, which means a single run cannot tell you whether the crew regressed or the sampler was unlucky.
Push as much of the crew as possible into deterministic code, then buy confidence about the rest with scenario coverage. Four layers, cheapest first:
Teams usually build layer 4 first, because it is the layer that looks like agent testing, then wonder why the feedback loop takes twenty minutes. Layers 1 and 2 catch a large share of real defects and cost nothing per run. Build up from the bottom.
A common complaint about CrewAI is that individual components are hard to unit test. The fix is structural rather than technical: keep the logic outside the agent. A guardrail function receives a TaskOutput and returns a tuple, so pytest can construct that input and call the function directly. No agent, no prompt, no key.
Here is a guardrail that rejects a pricing summary quoting a bare number with no currency marker, plus its tests:
from typing import Any, Tuple
from crewai import TaskOutput
def price_table_guardrail(result: TaskOutput) -> Tuple[bool, Any]:
"""Reject a task output that quotes a price without a currency symbol."""
text = result.raw.strip()
if not text:
return (False, "Task returned an empty output")
for token in text.split():
if token.replace(".", "", 1).isdigit() and "$" not in text:
return (False, f"Bare number '{token}' with no currency marker")
return (True, text)
def make_output(raw: str) -> TaskOutput:
return TaskOutput(description="pricing summary", raw=raw, agent="analyst")
def test_guardrail_passes_on_currency_marked_output():
ok, payload = price_table_guardrail(make_output("Starter costs $399 per month."))
assert ok is True
assert payload == "Starter costs $399 per month."
def test_guardrail_rejects_bare_number():
ok, reason = price_table_guardrail(make_output("Starter costs 399 per month."))
assert ok is False
assert "no currency marker" in reason
def test_guardrail_rejects_empty_output():
ok, reason = price_table_guardrail(make_output(" "))
assert ok is False
assert reason == "Task returned an empty output"Running that file against crewai 1.15.16 produces this output, which is the actual terminal result and not an illustration:
$ python -m pytest test_crew_guardrails.py -v
============================= test session starts =============================
platform win32 -- Python 3.13.5, pytest-9.1.1, pluggy-1.6.0
collected 3 items
test_crew_guardrails.py::test_guardrail_passes_on_currency_marked_output PASSED [ 33%]
test_crew_guardrails.py::test_guardrail_rejects_bare_number PASSED [ 66%]
test_crew_guardrails.py::test_guardrail_rejects_empty_output PASSED [100%]
============================= 3 passed in 10.89s ==============================Ten seconds is almost entirely CrewAI's import time; the assertions themselves are instant. Apply the same shape to custom tools: put the HTTP call, the parsing, and the error handling in a plain function, then have the tool wrap it. The wrapper is what the agent sees, and the function is what your tests call.
A guardrail is the one testing hook that also runs in production. Per the CrewAI tasks documentation, a function-based guardrail accepts exactly one parameter and returns (True, validated_result) on success or (False, error_message) on failure. When it fails, the error goes back to the agent, which retries until it passes or exhausts guardrail_max_retries, which defaults to 3.
That retry loop is why guardrails beat post-hoc validation. A failing assertion in a test tells you the crew was wrong yesterday; a failing guardrail tells the agent, mid-run, exactly what to fix.
from crewai import Task
pricing_task = Task(
description="Summarize the pricing tiers from the scraped page",
expected_output="A markdown table with every price carrying a currency symbol",
agent=analyst,
guardrails=[price_table_guardrail, no_placeholder_guardrail],
guardrail_max_retries=2,
)Three practical rules for writing them:
The guardrails list takes precedence over the singular guardrail argument, and guardrails run in sequence, so order them cheapest-check-first. A regex that fails in microseconds should never sit behind an LLM-evaluated string guardrail.
The most useful assertions about a crew are about behavior. Did the research agent call the live search tool, or did it answer from the model's own memory? Did the writer task run before the editor task? Wording changes every run; the tool-call sequence usually does not.
CrewAI exposes this through an event bus. The CrewAI event listener documentation covers events across crews, agents, tasks, tools, memory, LLM calls, and flows, including ToolUsageStartedEvent, TaskCompletedEvent, and CrewKickoffFailedEvent. You subclass BaseEventListener and register handlers in setup_listeners.
from crewai.events import BaseEventListener, crewai_event_bus, ToolUsageStartedEvent
class ToolCallRecorder(BaseEventListener):
"""Records every tool the crew reaches for, so tests can assert on tool choice."""
def __init__(self):
self.calls = []
super().__init__()
def setup_listeners(self, event_bus):
@event_bus.on(ToolUsageStartedEvent)
def _on_tool_start(source, event):
self.calls.append(event.tool_name)
def test_recorder_captures_tool_selection():
recorder = ToolCallRecorder()
future = crewai_event_bus.emit(
None,
ToolUsageStartedEvent(tool_name="serper_search", tool_args={"q": "crewai"}),
)
if future is not None:
future.result(timeout=5)
assert recorder.calls == ["serper_search"]Two details in that snippet are not in the documentation, and both cost real debugging time when this test was first written.
In a real suite, attach the recorder before kickoff() and assert on the sequence afterwards: that the search tool was called at least once, that no deprecated tool appeared, that the total call count stayed under a budget. Feeding the same event stream into a tracing backend gives you the production half of this picture, which we cover in agent observability.
CrewAI ships a scoring command. The CrewAI testing documentation describes it as running the crew for a number of iterations and printing a table of per-task scores on a 1 to 10 scale, per-run columns, an average per task, the responsible agent, a crew average, and execution time.
The docs page lists two parameters. The shipped CLI in version 1.15.16 lists three:
$ crewai test --help
Usage: crewai test [OPTIONS]
Test the crew and evaluate the results.
Options:
-n, --n-iterations INTEGER Number of iterations to Test the crew
-m, --model TEXT LLM Model to run the tests on the Crew. For now
only accepting only OpenAI models.
-f, --filename TEXT Crew-only: path to a trained-agents pickle
(produced by `crewai train -f`). When set,
agents load suggestions from this file instead
of the default trained_agents_data.pkl.
--help Show this message and exit.The documented defaults are out of date. The docs page states 2 iterations and gpt-4o-mini. Reading the defaults straight off the installed command object in 1.15.16 returns n_iterations = 3 and model = gpt-5.4-mini. That gap matters because iteration count drives token spend and the scoring model drives the scores themselves, so a version bump can silently change both your bill and your numbers.
Pin both explicitly and never rely on the default:
crewai test -n 5 -m gpt-4oThree limits to plan around. The scorer is OpenAI-only, stated verbatim in the CLI help, so a crew running on Claude or a local model is graded by a judge from a different family. The score is an LLM judgment, not ground truth, which makes it useful as a trend and misleading as a gate. And it scores task output only, so it cannot see that the agent reached the right answer through the wrong tool.
Use it as a regression signal: record the crew average per release, and investigate a drop of more than a point rather than reacting to any single low run.
Everything above tests the crew as a Python object. The moment it sits behind a chat endpoint, a support widget, or a voice line, none of it reaches the thing users touch. Guardrails cannot tell you the crew is condescending to a confused customer, and a 1 to 10 task score cannot tell you it leaked a policy detail on turn four of a nine-turn conversation.
That layer needs synthetic users holding real conversations. TestMu AI's Agent Testing platform connects to a live chat, voice, or phone endpoint and runs 15+ specialized evaluators against it in parallel, each probing one failure mode: a Hallucination Hunter for invented facts, a Bias Detector for differential treatment, a Context Specialist for whether the crew still remembers turn two, plus completeness, tone, escalation, and prompt-injection checks. Upload the PRD or policy doc the crew was built from and the platform generates 60 to 100+ scenarios spanning happy paths, edge cases, adversarial inputs, and compliance checks.
Two capabilities matter most for a CrewAI deployment. 10 pre-built personas (Confused Customer, Angry User, International Caller, Off-Script User, and others) exercise the inputs a developer testing their own crew never types. And the run resolves to a single Green, Yellow, or Red readiness verdict instead of a scores dashboard, so a release decision does not require reading every transcript. Connect a crew's HTTP endpoint using the chat agent API integration docs.
Note: A CrewAI crew behind a chat endpoint needs the same scenario coverage as any production agent. TestMu AI runs 15+ autonomous evaluators against your live endpoint and returns a Green, Yellow, or Red readiness verdict before you ship. Try TestMu AI free!
The layers have different costs, so they need different triggers. Running everything on every push burns tokens and slows reviews to the point that people stop reading the results.
| Layer | Trigger | Fails the build when |
|---|---|---|
| pytest on tools and guardrails | Every push and pull request | Any assertion fails. No API key needed, so it runs on forks too. |
| Event-bus behavioral tests | On merge to main | A required tool was never called, or the tool-call budget was exceeded. |
| crewai test scoring | Nightly and pre-release | Crew average drops more than an agreed threshold against the last baseline. |
| Scenario evaluation on the endpoint | Pre-release and after any prompt or model change | The readiness verdict is Red, or a Critical-risk scenario fails. |
A practical trick for the nightly job: use CrewAI's kickoff_for_each, which executes the crew once per item in a list of inputs, to run a fixed regression set of prompts in one command and diff the tool-call sequences against the previous night. Behavioral drift shows up there days before a quality score moves.
Keep the baseline in version control next to the crew definition. A prompt edit that changes the crew average is information; a prompt edit that changes it without anyone noticing is a regression. The same discipline applied to conventional suites is covered in agent functional testing.
If your crew is one of several frameworks in play, the trade-offs between them are covered in our guide to agentic AI frameworks, which walks through where role-based crews fit against graph-based and conversational alternatives. For the conversational side specifically, AutoGen testing covers the equivalent hooks in that framework.
Start by moving one tool body out of its CrewAI wrapper into a plain function and writing three pytest cases against it. That takes an afternoon, needs no API key, and gives you the first hard pass or fail signal your crew has ever had. Then attach a guardrail to the task whose output feeds the most downstream work, since that is where a bad shape does the most damage.
Once the deterministic layers are in place and your crew is answering real users, the remaining risk is scenario coverage. Point TestMu AI's Agent Testing at the live endpoint, upload the document the crew was built from, and let the platform generate and score the adversarial conversations nobody on the team would think to type. The Agent Testing platform getting started guide covers connecting an endpoint and reading your first readiness verdict.
Author
Samyak Goyal is a Senior Member of Technical Staff at TestMu AI engineering Kane CLI, the command-line tool that runs browser automation from the terminal, where a flow described in natural language executes in a real Chrome browser and returns pass or fail with shareable proof. He is a backend engineer with 4+ years of experience, previously an SDE at Innovaccer, where he built APIs, introduced Kafka, and cut deployment from weeks to hours. Samyak also builds multi-agent systems, skill-orchestration frameworks, and a personal copilot that indexes 200+ microservice repositories.
Reviewer
Anubhav Singhmaar is an AI Product Manager at TestMu AI driving Kane CLI, the command-line tool that brings browser automation to the terminal, turning natural-language flows into runs in a real Chrome browser that return pass or fail with shareable proof. He owns the roadmap and prioritization and works with engineering to ship developer-facing features. Before TestMu AI, he spent over four years at Sprinklr owning enterprise voice AI across APAC and EMEA. A mechanical engineer turned product manager, he grounds guidance in real QA workflows.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance