Unit 04.04: Testing a tool without the agent
Tools are ordinary functions. That makes them the one part of a crew you can test exhaustively, for free, without a model or an API key.
Call it directly, assert on the result
A CrewAI tool exposes .run(), so a test is a dictionary of arguments and an expected string. No agent, no crew, no key.
The code runs three cases against a conversion tool, including two refusals.
from crewai.tools import tool
RATES = {"USD": 1.0, "EUR": 0.92}
@tool("convert")
def convert(amount: float, currency: str) -> str:
"""Convert an amount from the given currency to USD."""
if currency not in RATES:
return f"REFUSED: no rate for {currency}"
if amount < 0:
return "REFUSED: amount must not be negative"
return f"{amount * RATES[currency]:.2f} USD"
CASES = [
({"amount": 100.0, "currency": "EUR"}, "92.00 USD"),
({"amount": 100.0, "currency": "JPY"}, "REFUSED: no rate for JPY"),
({"amount": -5.0, "currency": "USD"}, "REFUSED: amount must not be negative"),
]
passed = 0
for args, expected in CASES:
got = convert.run(**args)
ok = got == expected
passed += ok
print(f"{'PASS' if ok else 'FAIL'} {str(args):42} -> {got}")
print(f"\n{passed}/{len(CASES)} tool cases pass -- no agent, no model, no key")
# Tools are ordinary functions. Test them like ordinary functions, exhaustively
# and for free, before any agent is allowed near them.
All three cases run in milliseconds and cost nothing. Two of them test refusal paths - the unsupported currency and the negative amount - which are exactly the paths a successful crew run never exercises.
That is the argument for testing tools separately. A crew run tests the happy path through whichever tools the agent happened to choose; a direct test covers every branch you wrote, including the ones that exist specifically for inputs a model might produce.
The mistake this prevents
The mistake is testing tools only through the agent. It is slow, it costs tokens, it is non-deterministic, and it systematically fails to reach the refusal branches - because a well-behaved agent does not produce the malformed input those branches exist for.
Takeaway
Test tools directly with .run(), covering every refusal branch. It is free, deterministic and exhaustive, and it is the only part of the crew where all three are true.
