AI Agents for QA and Software Testing
The contrarian truth about an AI testing agent: it shouldn't write more tests. It should write the ones humans skip - edge cases, error paths, and boundaries.
import anthropic
client = anthropic.Anthropic()
SYSTEM = """You are a QA engineer generating tests. You are given
a function's SPECIFICATION (what it should do) and its signature -
NOT its implementation. Generate tests that verify the spec,
focusing on the cases developers commonly miss:
- Boundary values (empty, zero, max, off-by-one)
- Invalid and malformed inputs
- Error conditions and how failures should be handled
- Edge cases implied by the spec but easy to forget
For each test, state the input, the expected behavior per the
spec, and WHY this case is easy to get wrong. Do not test that
the code does what the code does - test that it does what the
SPEC says."""
def generate_tests(spec, signature):
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM,
messages=[{
"role": "user",
"content": f"SPECIFICATION:\n{spec}\n\nSIGNATURE:\n{signature}"
}],
)
return msg.content[0].textHere's a contrarian take that will save you from a common trap: the goal of an AI testing agent is not to write more tests. Teams that point an agent at their code and say "generate tests" end up with hundreds of tests that assert the code does what it currently does - including its bugs - and that inflate the coverage number while catching nothing. More tests is the wrong target. The right target is the tests humans skip: the edge cases, the error paths, the boundary conditions, the ugly inputs nobody wants to think about. That's where bugs actually live, and it's exactly where a tireless agent has an edge. Here's how to build one.
Quick-Start: Copy This Right Now
[object Object], anthropic
client = anthropic.Anthropic()
SYSTEM = ,[object Object],
,[object Object], ,[object Object],(,[object Object],):
msg = client.messages.create(
model=,[object Object],,
max_tokens=,[object Object],,
system=SYSTEM,
messages=[{
,[object Object],: ,[object Object],,
,[object Object],: ,[object Object],
}],
)
,[object Object], msg.content[,[object Object],].textWhat this does: it generates tests from the specification rather than the implementation, aimed deliberately at boundaries, invalid inputs, and error paths - so the tests can actually catch where the code diverges from what it's supposed to do, instead of certifying its current behavior.
Understanding the Variables
The most important design choice is in the input: you feed the agent the spec and signature, not the implementation. This is the whole ballgame, and it's counterintuitive enough that most teams get it backwards.
If you show the agent the code and ask for tests, it writes tests that pass. It reads what the function does and asserts that it does that. If the function has a bug - returns the wrong value on empty input - the generated test will assert the buggy behavior is correct, locking the bug in place and giving you false confidence. You've encoded the implementation's mistakes as requirements.
If instead you show the agent only what the function is supposed to do, it generates tests against the intended behavior. Now when the implementation diverges from the spec, the test fails - which is the entire purpose of a test. The agent becomes an independent check on the code rather than a mirror of it.
The spec is therefore the critical input, and its quality determines everything. A vague spec produces vague tests. This has a useful side effect: writing a spec precise enough for the agent to test forces you to actually decide what the function should do at the boundaries, which is often where the real ambiguity - and the real bugs - hide.
⚡ Pro tip: when you don't have a written spec, have the agent draft one from the function name, signature, and docstring first, then review and correct it, then generate tests from your corrected version. The spec-drafting step surfaces disagreements about intended behavior before a single test is written.
Step-by-Step: Building an AI Testing Agent That Finds Bugs
First, gather the spec and signature for the unit under test and deliberately withhold the implementation. If you're testing an existing function, write down what it's supposed to do from the outside - contracts, documented behavior, ticket requirements - without copying its internal logic.
Second, generate the tests with the focus on the missed cases: boundaries, invalid inputs, error handling. The agent's advantage over a tired human is that it never gets bored enumerating the ugly cases - the empty list, the Unicode string, the negative number, the simultaneous-request race - that developers skip because they're tedious and feel unlikely.
Third, and this is the step that separates a real testing agent from a coverage-inflation machine: run the generated tests against the actual implementation and pay attention to the failures. A generated test that fails is the whole point - it's either found a real bug or a spec-versus-code disagreement worth resolving. Don't reflexively "fix" the test to pass; investigate whether the code or the test is right.
Fourth, verify the tests actually test something. A test that passes no matter what the code does is worthless. The quick check: temporarily break the function - flip a comparison, return a wrong constant - and confirm the test fails. A test that still passes against broken code isn't testing the behavior it claims to.
⚡ Pro tip: use the temporarily-break-it check as a routine gate, not a one-time thing. This is the core idea behind mutation testing - a test suite is only as good as its ability to detect broken code. If you can introduce an obvious bug and your tests stay green, your coverage number is a comforting lie.
⚡ Pro tip: have the agent explain why each edge case is easy to get wrong. That explanation is a mini code review - it surfaces the assumptions the implementation might have made, and often points straight at the bug before you even run the test.
Pro-Level Variations
For API and integration testing, feed the agent the API contract - the documented request and response schema - and have it generate tests for malformed requests, missing fields, wrong types, and boundary payloads. Contract-driven test generation catches the mismatches between what an endpoint promises and what it actually enforces.
For regression protection, when a bug is found and fixed, have the agent generate a test that captures that specific failure from the bug description, so it can never silently return. A testing agent is excellent at turning a bug report into a permanent guard.
For property-based testing, have the agent identify invariants from the spec - "the output is always sorted," "the total never exceeds the input sum" - and generate tests that check those properties across many random inputs. Invariants catch whole classes of bugs that example-based tests miss one at a time.
For legacy code with no spec and no tests, an AI testing agent can still help, but the workflow inverts. Here you deliberately let it read the implementation to generate characterization tests - tests that capture what the code currently does, bugs and all - as a safety net before you refactor. This is the one time testing against the implementation is correct, because the goal isn't to find bugs, it's to freeze current behavior so you can change the code without silently altering it. Just be honest about which mode you're in: characterization tests protect a refactor, they don't validate correctness, and confusing the two is how teams convince themselves untested legacy code is "well tested."
The Real Metric an AI Testing Agent Should Move
Step back from the tooling and ask what an AI testing agent is actually for, because the answer reshapes how you use it. It exists to catch bugs before they reach users, at lower human cost than writing every test by hand. That framing rules out the coverage-chasing trap immediately: a suite that hits 95% coverage while catching nothing has spent your compute and your reviewers' attention to move a number that doesn't correspond to safety.
The metric that does correspond to safety is escaped defects - bugs that made it past your tests into production - trending down over time. When a bug escapes, the discipline is to have the agent generate a regression test from the incident before you close it, so that exact failure is permanently guarded. Do this consistently and your test suite becomes a growing memory of every way your system has actually broken, which is far more valuable than a suite of hypothetical happy-path assertions. Each escaped defect makes the suite stronger instead of just embarrassing.
There's a subtler payoff in reviewer attention. When an AI testing agent handles the tedious enumeration of boundary and error cases, your human engineers spend their limited review energy on the tests that encode genuine judgment - the tricky business-logic cases, the security-sensitive paths, the concurrency scenarios where correctness is subtle. The agent does the breadth; humans do the depth. That division is where the real payoff lives, and it's invisible if you're only watching the coverage bar climb.
⚡ Pro tip: track how many of the agent's generated tests actually failed against real code and turned out to be finding real problems. That "tests that caught something" count is your true signal - a testing agent producing hundreds of always-green tests is inflating coverage, while one producing a handful of red tests that expose genuine spec-versus-code gaps is doing the job you built it for.
Troubleshooting Common Issues
If the generated tests all pass on the first run against real code, be suspicious rather than pleased - it often means the agent saw or inferred the implementation and wrote tests to match it. Double-check that you withheld the implementation and that the tests genuinely exercise boundaries.
If the tests are trivial - asserting obvious happy-path behavior - your spec is too thin or your prompt isn't pushing hard enough toward edge cases. Sharpen the instruction to prioritize the cases developers miss, and enrich the spec with the boundary behavior it's currently leaving implicit.
⚠️ Common mistake: measuring the testing agent by coverage percentage. Coverage counts lines executed, not bugs caught - and it's easy to hit 90% coverage with tests that assert nothing meaningful. The metric that matters is bugs caught before merge: how many real defects the generated tests surfaced that would otherwise have shipped. Optimize for coverage and you get a green number and a false sense of safety; optimize for bugs-caught and you get tests worth having.
Your Turn
Take one function with clear intended behavior, write down its spec without looking at its guts, and have the agent generate boundary and error-path tests from that spec. Run them, and treat every failure as a finding to investigate rather than a test to silence. You'll likely turn up at least one real disagreement between what the code does and what it should do.
The specs, the edge-case-focused prompt, and the break-it-to-check-it discipline are the reusable assets - they encode a testing philosophy that catches bugs instead of certifying them. Keeping your test-generation prompts and spec templates in a shared library like PromptABCD means every engineer generates tests the same rigorous way, against the spec and aimed at the ugly cases, rather than each person pointing an agent at their code and quietly locking in its bugs as requirements.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
