Writing Unit Tests with AI Assistance: Coverage That Actually Catches Bugs
A test suite can show full coverage and still miss the bug that breaks production on a Friday evening. Coverage counts lines that ran, not behaviour that was checked. AI assistants make it easy to generate dozens of tests in minutes, which makes the problem worse when every test merely repeats the happy path. The useful approach is slower and more deliberate: decide what behaviour matters, write tests for the cases that could go wrong, and let the assistant handle the repetitive drafting.
1. Start from behaviour, not from lines of code
Before asking an assistant for tests, write down in plain words what the function must do. A discount calculator must apply the correct rate, refuse expired codes, and never produce a negative total. Those three sentences are a better brief than the source code alone, because they state intent rather than implementation. Hand the brief to the assistant along with the code, and ask for one test per stated behaviour.
This order matters because an assistant shown only code will test what the code happens to do, including its mistakes. If the code applies the wrong rate, the generated test will often lock in that wrong rate and pass forever. Behaviour first keeps you in charge of correctness, and it gives you a checklist for reviewing whatever the assistant produces.
2. Give the assistant a failing example before it writes anything
One concrete example is worth a paragraph of instruction. Show the assistant an input, the output you expect, and one input that must be rejected. For a date parser, that might be a valid date, a leap-year edge case, and a malformed string that must raise an error. Concrete examples anchor the assistant to your real requirements and expose misunderstandings early, while they are still cheap to fix.
Keep the examples small and realistic. Use values from your actual domain, such as real product prices or genuine user inputs, rather than invented numbers. Realistic examples produce tests that read like documentation for the next developer, and they make it obvious when a generated test checks something nobody actually needs.
3. Cover the edges and the failure paths
Most real bugs live at boundaries: empty lists, zero amounts, maximum values, missing fields, and inputs that arrive in the wrong order. Ask the assistant explicitly for boundary tests and error tests, not just success tests. A good prompt names the categories: empty input, single item, large input, invalid type, and a dependency that fails. Without that nudge, assistants tend to generate a row of near-identical success cases.
For each failure test, check that the test fails for the right reason. A test that passes because the code throws any error, rather than the specific error you intended, will hide a real regression later. Ask the assistant to assert on the error type and, where it makes sense, the error message. This is the same discipline that makes a debugging workflow with an AI assistant effective: precise expectations, verified one at a time.
4. Keep every test readable on its own
A test that nobody can read will not be maintained, and an unmaintained test gets deleted at the first sign of trouble. Give each test a name that states the behaviour, such as a test for expired discount codes being rejected, rather than a generic label like test case three. Keep the setup short, use plain values, and avoid clever helpers that hide what is being checked.
When the assistant generates long, tangled tests, ask it to split them. One assertion per test is a good default, with a small number of closely related assertions allowed where they describe a single outcome. Short tests also fail more clearly: when one breaks, the name alone tells you what went wrong, which saves real time during a rushed release.
5. Run the tests, then deliberately break the code
A test suite that has never failed proves nothing. After generating tests, run them, confirm they pass, and then change the code in a small way that should break one test: flip a comparison, remove a validation check, or change a constant. If no test fails, you have found a gap, and that gap is exactly the kind of blind spot that reaches users.
This break-it check takes minutes and is the single most reliable way to judge generated tests. It also protects you from a subtle trap: assistants sometimes write tests that pass no matter what the code does, for example by asserting on a value computed by the same logic under test. A test must be able to fail, or it is decoration. The review habits in our guide to reviewing code securely with AI apply here too: trust the output only after you have checked it.
6. Treat generated tests as drafts that need your judgement
Assistants invent functions that do not exist, assume libraries you never imported, and describe behaviour from an older version of your code. Read every generated test against the real code before it enters your suite, and delete tests that check trivia. A suite of twenty sharp tests beats a suite of two hundred vague ones, both for speed and for the confidence it gives the team.
Finally, keep the tests alongside the code they cover and update them in the same change, not later. Documenting the intent behind tricky tests helps the next person, and our guide to documenting code with AI shows how to capture that intent without writing an essay. Tests, like documentation, rot when they are left behind by code changes.
Bottom line: Let AI draft your tests from a clear behaviour brief, then prove each test can fail, and keep only the tests you understand and trust.