Writing tests is one of those things most developers know they should do more of and genuinely don’t want to do. It’s not that testing is hard — it’s that writing thorough test suites for code you’ve just written is repetitive, context-heavy work that doesn’t feel creative. You know what the code does; now you have to write a series of variations that confirm it does that.
LLMs are surprisingly well-suited to exactly this kind of work. Not because AI-generated tests are always better than human-written ones — they frequently aren’t — but because AI can handle the mechanical parts (edge case enumeration, fixture creation, happy path coverage) quickly enough that you can spend your testing energy on the genuinely tricky cases that require understanding the business logic.
Here’s how to actually use AI test generation rather than just prompting your way to a false sense of coverage.
What AI Test Generation Is Actually Good At
Start with what LLMs do well in a testing context. Give a model a function signature and its implementation, and it’s excellent at generating:
Edge cases you might not have thought of — negative numbers, empty strings, zero values, Unicode characters, very large inputs. Boundary conditions — the value just inside and just outside every check. Happy path parameterised tests that cover multiple valid input combinations. Error cases — what should the function do when called with null, undefined, or the wrong type? Fixture and mock data that matches the shape of your interfaces.
This is genuinely useful. Many code coverage gaps come not from failing to think of these cases but from the friction of writing out forty it() calls covering all the variations. AI removes that friction.
Where AI struggles: tests that require deep understanding of external system behaviour, tests that encode business rules that aren’t obvious from the code itself, and tests for correctness in domains where you’d need to independently verify the expected output (cryptographic implementations, numerical precision-sensitive calculations). In those cases, AI-generated tests may be syntactically correct but logically wrong — they’ll pass without catching the bugs they should catch.
A Practical Workflow with Cursor or Claude Code
The most effective workflow I’ve seen is to write the implementation first, then hand it to an AI coding assistant for test generation, then review and curate the output.
In Cursor, the workflow is roughly: write your function, select it, and use cmd+K to ask for tests. Be specific in the prompt: “Write Vitest unit tests for this function including edge cases for empty input, null values, and values exceeding the maximum limit. Use describe/it blocks and include at least one parameterised test using test.each.” Specificity matters — a vague “write tests” prompt gets vague tests.
In Claude Code (via the CLI), you can do something similar by giving it the file and context:
claude "Write comprehensive Vitest tests for the function in src/utils/pricing.ts.
Include: positive cases for standard inputs, edge cases for zero and negative values,
error handling for invalid currency codes, and a test.each for multiple currency conversions.
Don't mock the formatting logic -- test through it."
The “don’t mock X” instructions matter. AI test generators have a tendency to mock things aggressively, which can produce tests that pass for the wrong reasons. If you want to test the actual behaviour end-to-end, be explicit.
Reviewing AI-Generated Tests
Never commit AI-generated tests without reading them. This sounds obvious but gets skipped in the rush to hit coverage targets.
What to check: Does each test’s assertion actually verify what the description says? Are the expected values correct, or did the AI make up plausible-looking numbers? Are the mocks realistic — do they return data that matches what the real dependency would return? Are there tests that are trivially true (assert that undefined === undefined) that don’t actually verify anything?
Run the tests, then mutate your source code and confirm the tests fail appropriately. If you can break the function in an obvious way without failing tests, those tests aren’t providing coverage worth having.
Mutation testing tools (Stryker for JavaScript/TypeScript, mutmut for Python) are excellent for verifying test quality after AI generation. They systematically introduce small bugs into your code and check whether your tests catch them. A suite of 50 AI-generated tests that mutation testing reveals as mostly ineffective is worse than 15 careful human-written tests that actually catch regressions.
The Test.each / Parameterised Pattern
One place where AI test generation genuinely shines is parameterised tests. Writing test.each tables in Vitest or @pytest.mark.parametrize in pytest is exactly the kind of repetitive data entry that AI handles well.
// Ask for this pattern explicitly
describe('formatCurrency', () => {
test.each([
[100, 'GBP', '£100.00'],
[99.99, 'GBP', '£99.99'],
[0, 'GBP', '£0.00'],
[1000000, 'GBP', '£1,000,000.00'],
[100, 'EUR', '€100.00'],
[-50, 'GBP', '-£50.00'],
])('formats %s %s as %s', (amount, currency, expected) => {
expect(formatCurrency(amount, currency)).toBe(expected);
});
});
Getting an AI to generate this table with twenty or thirty cases — especially for functions with multiple input dimensions — is a much better use of the technology than having it write boilerplate individual test functions.
When to Write Tests Yourself
For anything involving business logic that isn’t obviously derivable from the code, write your own tests first. If the correct behaviour requires knowing that “when a customer has been with us more than 3 years they get the legacy pricing tier”, that context isn’t in the function signature and AI will guess at it or ignore it.
Tests for security-sensitive code paths — authentication, authorisation, input validation — deserve human-written tests that deliberately encode the threat model. An AI generating tests for a permission check might verify that an admin can access the resource; it might not think to verify that a non-admin can’t.
The mental model that works best: use AI for the mechanical test matrix (all the combinations, all the edge cases) and write yourself the tests that encode “here is what this system must never do.” Both kinds of test are valuable; the first is tedious and delegatable, the second requires understanding that only you have.
Done well, AI-assisted test generation actually shifts how you think about testing — from a chore you do after writing code to a collaborative design activity where you describe the expected behaviour and the AI drafts the verification scaffolding. That’s a genuinely useful change in the workflow, not just a productivity shortcut.