LLM-Based Test Generation: Useful, With Caveats
Large language models can draft tests from a description, a spec, or existing code. Here's where that genuinely helps, the failure modes to watch for, and how it differs from autonomous testing.
Ask a model to "write tests for this function" and you get plausible-looking tests in seconds. That is genuinely useful and genuinely dangerous, and which one it turns out to be depends entirely on what you do with the output.
What LLM test generation is good at
- First drafts. Given a function, a component, or a spec, a model produces a reasonable starting set of tests — the obvious cases, the fixture setup, the framework boilerplate. On a well-specified unit, that's most of the mechanical work done in seconds.
- Filling in coverage. Point it at code with thin tests and it will suggest cases you skipped — an error path, a null input, a second branch.
- Translating intent into scaffolding. Given acceptance criteria in plain language, it can produce test structure that matches, which you then flesh out with real assertions.
- Boilerplate-heavy frameworks. The more ceremony a framework demands — mocks, providers, test harness setup — the more a model saves you, because that part is patterned and repetitive.
The failure modes
- It tests what the code does, not what it should do. This is the big one. A model reads the implementation and writes assertions that match the current behaviour. If the code has a bug, the generated test faithfully encodes the bug as "expected," and now you have a green test actively protecting a defect.
- It misses the cases that matter. Empty inputs, concurrency, permission boundaries, expired sessions, the second-of-two-users scenario — the cases that come from thinking about risk. Models generate the typical; bugs live in the atypical.
- Confident but wrong assertions. Off-by-one expected values, a slightly wrong error message, an assertion that happens to pass for a reason unrelated to what it's supposedly checking. See false positives vs false negatives.
- Over-mocking. Models mock freely, and a test that mocks everything ends up verifying "my code called these functions in this order" rather than "my code produced the right result." It passes, it's brittle to refactors, and it proves little. See mock vs stub vs fake.
- Plausible structure hiding shallow checks. The test looks thorough — good names, arrange-act-assert, multiple cases — but every case asserts something trivial. Reviewers pattern-match on the shape and approve it.
How to use it safely
- Treat generated tests as a draft to review, never as done. The same discipline you'd apply to reviewing AI-generated production code applies here.
- Verify each assertion is actually right — check the expected values against the spec or your own reasoning, not against what the code returns.
- Add the cases it missed, especially failure cases and boundaries. This is where your judgement earns its keep.
- Check the mocking. Replace excessive mocks with fakes or real dependencies wherever practical, so the test exercises real behaviour.
- Be extra sceptical of tests for code the same model just wrote. If the assistant wrote the function and the tests, both share the same misunderstanding of your intent, and the tests will happily confirm the wrong behaviour.
LLM generation vs autonomous testing
Both involve AI, but they produce different things:
- LLM test generation produces test code that you then own, review, and maintain. It's an authoring accelerant for a scripted suite — it makes writing the suite faster, not maintaining it cheaper.
- Autonomous testing produces coverage with no test artifact to maintain. The tool explores the running app itself and reports what broke; there's no generated script to review or keep in sync.
They coexist well: use a model to draft the exact scripted assertions for your critical business rules, and autonomous runs for the broad end-to-end coverage. See AI in software testing for where each fits in the wider picture.
Bottom line
LLM test generation is a strong first-draft tool with one dangerous default: it assumes the current code is correct and writes tests to match. Review every generated test as carefully as you'd review the code, verify the assertions, add the risk-driven cases it skips, and fix the mocking. Used that way it's a real accelerant. Used as "generate and merge," it manufactures green checks that protect bugs.