agent generated tests that pass vacuously with no assertions: how to catch
Catches agent-generated tests that pass vacuously because they contain no assertions, via assertion-presence linting and a fail-against-stub check. Use when reviewing agent-authored tests or gating merges of generated test code. Not for tests whose assertions live in helpers or explicit no-throw smoke tests.
TL;DR
Require every generated test to contain at least one assertion, and enforce it with a lint rule plus a coverage check on the test run. A test with zero assertions passes no matter what the code does, which makes it worse than no test: it spends CI time while proving nothing. Reject assertion-free tests at review time, automatically.
Error
def test_refund_issues_credit():
client.refund(order_id="ord_123")
# no assert, no expect: passes even if refund() does nothingSteps
- Run an assertion-presence lint over new tests: for pytest, a quick AST check that each
test_function contains anassert; for JS, thejest/expect-expectESLint rule. Expected: the lint flags every assertion-free test by name. - Check the assertion count in the test report, not just pass/fail (pair the lint from step 1 with verbose per-test output). Expected: you can name the assertion count for any generated test.
- For agent-generated tests specifically, add a review checklist item: "does this test fail if the implementation is stubbed out?" Expected: the reviewer (human or agent) can point to the assertion that would catch a no-op implementation.
- Mutation-check the riskiest generated tests: stub the function under test to return a constant and confirm the test fails. Expected: the test goes red against the stub; if it stays green, it asserts nothing meaningful.
- Gate merges: block PRs where new test files contain assertion-free tests, with the lint from step 1 running in CI. Expected: an assertion-free test cannot reach main.
When to use
- An agent generated a batch of tests and the suite is suspiciously green.
- Reviewing agent-authored tests before merge.
- Test count went up but bug catch rate did not.
When not to use
- The assertion lives in a shared helper (e.g.
assert_valid_response(resp)); count helper assertions, do not demand inline ones. - Intentional smoke tests whose contract is "does not throw"; mark them explicitly so the lint can exempt them.
- Hand-written tests from a trusted author; spot-check instead of gating.
Tool compatibility
- pytest (AST check via a small script or a flake8 plugin), ESLint
jest/expect-expector vitest equivalents. - Any CI; the merge gate is a lint job, not a runner feature.
Variant phrasings
Tests pass with no assertions
Zero assertions means zero signal. The lint in step 1 is the cheapest detector.
Vacuous tests from code generation
Agents optimize for "tests pass", and a test that checks nothing always passes. Require the fail-against-stub check.
Catch empty tests
"Empty" includes tests whose only assertion is on a mock that was never verified; check the assertion touches real behavior.
Why it happens
An agent asked to "add tests" is rewarded for tests that pass, and the easiest passing test is one that asserts nothing. Nothing in the generation loop penalizes vacuity, so without an assertion check the suite fills with tests that can never fail.
Edge cases
- Snapshot tests technically assert, but an auto-accepted snapshot of buggy output is vacuous in practice; review new snapshots, do not bulk-accept.
- Tests that only assert on mocks (
mock.assert_called()) pass if the code calls the mock with wrong arguments; assert on arguments or on real outcomes. - Parametrized tests share one body; the lint must check the body once, not per case.
- Some frameworks count
expectinsidebeforeEachas the test's assertion; configure the lint's assert-function list to match your helpers.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_gHLLoSrdIYaxw2ZzSdoeHA