AI can write unit tests. That is no longer the interesting question. The interesting question is which of the three available approaches fits your codebase, and what each one costs you in review time.
This guide compares the options, sets out what to evaluate before committing, and covers the failure mode nobody mentions in the demos: tests that pass, read well, and assert nothing useful.
The three approaches, and how they differ
"AI unit testing" describes three quite different things. Choosing between them is the actual decision.
They fail in different ways, which matters more than how they succeed.
Assistants fail by being agreeable. Ask for tests and you get tests, including for code that should not work the way it does. The tool has no opinion about whether the behaviour is correct.
Generators fail by encoding bugs as expected behaviour. If your function returns the wrong value, a generated test asserts that it returns the wrong value, then goes green forever. You now have a regression test protecting a defect.
Coverage tools fail by optimising for the metric. Line coverage rises, and the tests that raise it are the ones that execute code without checking anything meaningful about the result.
Start with what your tests are for
Before evaluating any tool, be clear about which job you are trying to do, because the tools are good at different ones.
Raising coverage on legacy code you will not change. Generation works well here. The current behaviour is the specification whether you like it or not, and pinning it down protects you during later refactoring. This is the strongest case for AI-generated tests.
Writing tests for new code as you build it. Assistants work well. You know what the code should do, the tool handles the boilerplate, and you supply the judgement about what to assert.
Finding bugs. No AI approach does this reliably. Generated tests describe existing behaviour rather than questioning it. If you want bug discovery, property-based testing and fuzzing are better tools, and a human reading the code is better still.
Satisfying a coverage gate. Achievable, and the least valuable thing on this list. Be honest with yourself about whether the gate is the goal or a proxy for the goal.
What to evaluate
Run any candidate tool against your own code before deciding. Vendor demos use clean, small, well-named functions, and your codebase is not that.
Pick three functions deliberately:
- A simple pure function with clear inputs and outputs. Everything passes this; it establishes the baseline.
- A function with a dependency on a database, HTTP call or the clock. This is where you find out whether the tool mocks sensibly or writes a test that hits your staging environment.
- Your worst function. The long one with six parameters, three side effects and a name that no longer describes what it does. This is where tools separate.
For each generated test, ask these questions.
Does it assert behaviour or implementation? A test that asserts a specific internal call sequence breaks every time you refactor. A test that asserts the returned value survives refactoring. Generated tests skew towards implementation because implementation is what the model can see.
Would it catch a real bug? Change the function to return the wrong value deliberately and re-run. If the test still passes, it was never testing anything. This is the single most useful check you can run, and it takes a minute.
Are the mocks honest? A mock that returns exactly what the code expects, in exactly the shape the code expects, tests nothing about how the code handles reality.
Can you read it in six months? Generated tests often carry names like test_process_data_case_3. That name tells a future reader nothing about what broke when it goes red.
The review cost is the real cost
Every team that adopts AI test generation discovers the same thing: generating tests is fast and reviewing them is not.
A generated test suite of 200 tests requires someone to read 200 tests and decide whether each asserts something worth asserting. That review is slower per test than writing the test would have been, because reading unfamiliar code and judging its correctness is harder than writing code you already understand.
This does not mean the approach fails. It means the saving is smaller than the demo suggests, and it lands in a different place than you expect. Be sceptical of any evaluation that measures tests generated per hour rather than reviewed tests merged per hour.
Two things make the review cheaper:
Generate in small batches. Twenty tests reviewed properly beats two hundred skimmed. Reviewer attention degrades fast on repetitive material.
Mutate before you review. Deliberately break the code under test and discard every generated test that still passes. This removes the worthless tests automatically and leaves your reviewer a much smaller, higher-quality set. It is the step that saves the most review time, and it is routinely skipped.
Working with an assistant, concretely
If you take the assistant route, the quality of what you get back tracks the quality of what you ask for. GitHub's own guidance notes that Copilot performs well on basic functions while complex scenarios require more detailed prompts and deliberate task breakdown.
That matches what we see in practice. A request like "write tests for this function" produces happy-path tests. Getting useful coverage means naming the cases:
- Ask for boundary conditions explicitly: empty input, single element, maximum size, null where null is permitted
- Ask for error paths by name: what happens when the dependency times out, when the input fails validation, when the record is missing
- State your testing conventions in the request, since the tool cannot infer your fixtures or naming scheme
- Generate one function's tests at a time; quality degrades noticeably as the requested scope grows
If you use pytest, say so and name the mechanisms you want. Fixtures are the standard way to supply test dependencies, and a request that mentions them produces something closer to your existing suite than a request that does not.
From our work
The teams that got the least from it were the ones that set a coverage target and let the tool meet it.
A decision framework
If you want a short version:
Choose an assistant when developers are actively writing the code, you want to keep judgement with the human, and your main cost is boilerplate.
Choose a generator when you have substantial untested legacy code, current behaviour is an acceptable specification, and you have review capacity for what it produces.
Choose coverage-driven generation only when an external requirement forces a coverage number and you have accepted that the number is the deliverable.
Choose none of them when your testing problem is flaky tests, slow suites or unclear requirements. AI generation makes all three worse by adding volume.
Questions worth asking a vendor
- What happens to our code? Is it used for training, retained, or sent to a third-party model provider?
- Can it run entirely within our infrastructure, and what does that cost?
- Which languages and frameworks are genuinely supported versus nominally supported?
- Does it integrate with our existing test runner, or does it produce a separate suite we have to maintain alongside?
- What is the licensing position on generated code?
That last question deserves a real answer rather than reassurance, particularly if your generated tests will be distributed with your product.
If you are weighing this up alongside your wider automation strategy, our guide to test automation frameworks covers the surrounding decisions, and Python automation testing covers the language most of these tools handle best.
If you would like an independent review of your test suite before adding AI generation to it, talk to our automation testing team.