Where "just write some tests" goes wrong
Hand a function to an LLM and ask it to "generate unit tests to improve coverage," and the most common outcome isn't tests that are merely mediocre — it's tests that look fine but test nothing at all:
def test_calculate_discount():
result = calculate_discount(100, 0.1)
assert result is not None # this assertion will never fail
def test_process_order():
# doesn't even compile: the mock's return type doesn't match the real interface
mock_client = Mock(return_value="success")
process_order(mock_client)These tests have coverage numbers attached to them — sometimes they even make the coverage report look "better" — but they provide zero protection against real regressions. assert result is not None will never fail, which means it asserts nothing. Worse still is a second failure mode: LLM-generated tests sometimes don't even compile or run, and dropping them straight into CI just manufactures new red X's.
Meta's 2024 paper "Automated Unit Test Improvement using Large Language Models at Meta" tackles exactly this problem, with a tool called TestGen-LLM. Its core idea isn't "train a smarter model to write tests" — it's the opposite: assume the LLM's output is probably garbage, build a mechanical filter pipeline that discards anything substandard, and keep only the small fraction that can prove its own value.
TestGen-LLM's four-layer filter
The paper calls this approach Assured Offline LLM-Based Software Engineering (Assured LLMSE): the LLM is just one generation step in a pipeline, and its output is never trusted directly — it has to pass a series of verifiable checks, and only what clears every check gets recommended to a human. That word "assured" is the key distinction from "just use the model's output as the result."
Concretely, this is a four-stage cascade of elimination:
LLM generates a candidate test
↓
① Build Filter
The candidate code must fully compile within the app's existing build infrastructure
Fails to build → discarded immediately
↓
② Pass Filter (first execution)
A test that compiles must pass on its first run
— this step doesn't try to judge "did the failure reveal a real bug,"
because there's currently no automated way to distinguish
a real bug from a bad assertion, so the conservative default is:
fails on first run → discard
↓
③ Flakiness Filter (stability)
Run the test 5 times in a row — it must pass every single time
Even one failure out of five → deemed flaky, discarded
↓
④ Coverage Filter
Must prove it actually increases code coverage
Fails to improve coverage → discarded, even if it passed the first three filters
↓
Only tests that survive all four layers get recommended to an engineer for reviewThe design philosophy is narrow the funnel at every stage — it's better to lose a valuable test than let a garbage test through. Layer three is particularly worth noting — running a test five times to filter flakiness is a technique Qodo Cover's roadmap later borrowed directly (more on that below).
One point worth clarifying, since it's easy to get wrong: mutation testing is not part of TestGen-LLM's filter pipeline. The paper does mention that mutation coverage would theoretically be the best-performing criterion, but it explicitly lists this as future work, noting that "it is challenging to deploy such computationally demanding techniques at the scale we would require." In other words, the mutation-testing idea is directionally right, but too expensive to run in practice — so this version relies on the much lighter "did coverage improve" signal instead of the more precise but far costlier "were the mutants of each line actually killed."
Real numbers: how this performed inside Meta
The paper reports two sets of numbers — one from a lab-style evaluation, one from real engineer usage — and the gap between them is itself informative.
Lab evaluation: Instagram's Reels/Stories components
Tested on 86 Kotlin components (31 Stories, 55 Reels):
75% of test classes: got at least one new test case that compiles correctly
57% of test classes: compiles AND passes reliably (cleared the first three filters)
25% of test classes: compiles, passes reliably, AND increases line coverage (cleared all four filters)This tells a very practical story: there's a big drop-off between "can an LLM generate a test" and "can an LLM generate a test that's actually worth something." Three-quarters of components got a test that "looks like it compiles," but only a quarter actually improved coverage — the middle three filters are precisely what's cutting away the "looks useful but isn't" output.
Real-world usage: three test-a-thons
More convincing than the lab numbers is data from three internal test-a-thons — sprint events where engineers actually reviewed the generated diffs:
Diffs generated Accepted by engineers Acceptance rate
──────────────────────────────────────────────────────────────────────────────
Instagram (1st round) 17 16 94%
Instagram (2nd round) 42 36 86%
Facebook 280 144 51% (~70% among reviewed diffs)
──────────────────────────────────────────────────────────────────────────────
Total: 196 out of 1,979 test classes were successfully improved (~10%)
73% of TestGen-LLM's improvement suggestions were accepted for productionComparing the two datasets side by side: in the lab, roughly 25% of test classes "clear all four filters." In the real-world test-a-thons, 73% of improvements were accepted by engineers. These numbers don't contradict each other — the filters eliminate output that's mechanically substandard (doesn't build, unstable, doesn't improve coverage), while "engineer acceptance" measures whether a test that's already cleared the mechanical bar is actually worth merging. In other words, the four filters solve "don't let garbage through" — they don't guarantee "everything generated is valuable." That judgment still needs a human.
The open-source implementation: how Qodo Cover turns this into a CLI tool
TestGen-LLM itself is an internal Meta tool and was never open-sourced. Qodo Cover (formerly Codium's Cover-Agent) is the first public open-source implementation of this same approach, runnable directly against your own project.
Its architecture maps the paper's "filter" concept onto four concrete components:
Prompt Builder
Gathers necessary context from the codebase (source file, existing tests,
coverage report) and assembles it into a prompt for the LLM
↓
AI Caller
Calls the LLM with the assembled prompt to generate a candidate test
(via LiteLLM, so the underlying model provider is swappable)
↓
Test Runner
Actually runs the project's test command against the newly generated test
↓
Coverage Parser
Parses the coverage report (Cobertura/JaCoCo, etc.) and checks
whether coverage genuinely increased
↓
Passes → keep the test, move to next iteration
Fails → discard, generate a new candidateThis is effectively a concrete implementation of TestGen-LLM's Pass Filter + Coverage Filter (the Build Filter is implicit in a failed Test Runner execution; a Flakiness Filter is on the roadmap, explicitly described as planned to "check test flakiness, e.g. by running 5 times as suggested by TestGen-LLM").
Usage is a CLI-driven loop: specify the source file, test file, coverage-report path, a target coverage percentage (--desired-coverage), and a max iteration count (--max-iterations); the tool repeatedly generates candidate tests, runs them, and checks coverage until it hits the target or runs out of iterations. This loop can be wired straight into a CI pipeline, or run locally as a one-shot "bump this module's coverage" command.
Worth flagging: Qodo Cover, the open-source project, is no longer actively maintained as of mid-2025 (the README explicitly marks it "no longer maintained" and suggests forking if you want to keep developing it). That doesn't diminish its value as a reference implementation of the TestGen-LLM approach, but it does raise a maintainability risk if you'd depend on it in production — a recurring theme in this series: open-source proof-of-concept projects and production-ready tools are often not the same thing.
Where this approach actually holds up
Putting Meta's lab data and test-a-thon data together, you can map out where this method genuinely works and where it doesn't.
Works well: modules with relatively self-contained logic and clear dependencies — the LLM can reasonably infer input/output relationships from function signatures and existing test style, and the four-layer filter effectively strips out most garbage output.
Doesn't work well:
- Code that depends heavily on external state or complex mocks — LLM-generated mocks frequently don't match the real interface's actual behavior, which tends to get caught by the Build Filter or Pass Filter, resulting in a low survival rate
- Code that requires deep business context to write a meaningful assertion — a test might "run successfully" but assert something meaningless (like
assert result is not Nonein the opening example). The coverage filter only checks whether the code path was executed, not whether the assertion itself means anything — this is a blind spot the four-layer filter never addresses - Scenarios demanding extreme determinism and reproducibility — the flakiness filter only runs five times, so statistically it can still miss a test that's fundamentally unstable
One point worth emphasizing: the four-layer filter guarantees that a test is mechanically valid — it does not guarantee that the test's assertions are semantically correct for the business logic. Coverage improvement is an automatable proxy metric, but "did this test actually check the thing I care about" still needs human review. That's exactly why Meta's own data treats "cleared all filters" and "accepted by engineers" as two distinct numbers that each need to be examined separately.
Summary
- LLM-generated tests are probably garbage by default — they may not compile, assert something trivial, or contribute nothing to coverage. TestGen-LLM's fix isn't a smarter model, it's a four-layer mechanical filter (build, first-run, stability, coverage) that eliminates substandard output stage by stage
- Mutation testing is not currently part of TestGen-LLM's methodology — the paper explicitly lists it as future work constrained by computational cost, and uses the much lighter "did coverage improve" signal instead
- The lab number (25% clear all filters) and the real-world number (73% accepted by engineers) measure two different things — the former is mechanical validity, the latter is human judgment of value, and the four-layer filter only solves the former
- The open-source implementation Qodo Cover turns this idea into a runnable CLI/CI iteration loop, but the project itself is no longer maintained — maintainability risk needs to be weighed before relying on it in production
Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.
Find more useful knowledge and interesting products on my Homepage