## 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

```text
def test_refund_issues_credit():
    client.refund(order_id="ord_123")
    # no assert, no expect: passes even if refund() does nothing
```

## Steps

1. Run an assertion-presence lint over new tests: for pytest, a quick AST check that each `test_` function contains an `assert`; for JS, the `jest/expect-expect` ESLint rule. Expected: the lint flags every assertion-free test by name.
2. 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.
3. 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.
4. 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.
5. 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-expect` or 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 `expect` inside `beforeEach` as 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
