Skip to content

How to Write Tests With AI That Actually Catch Bugs

Agents love to write tests that always pass. How to get real coverage — edge cases by name, no weakened assertions, tests that fail when the code breaks.

The Vibe Father 7 min read

Playbook

Ask an agent to "add tests" and you'll get tests. They'll be green, they'll look thorough, and a frightening number of them assert nothing. They call the function, get a result, and check that the result equals whatever the function happened to return — a test that can never fail because it was written to match the code, not the requirement. Worse, when the agent hits a case where the code is actually wrong, it will sometimes "fix the test" by weakening the assertion until the suite goes green. You end up with a wall of passing tests and zero protection.

Real coverage doesn't come from asking for more tests. It comes from telling the agent exactly what to test and refusing tests that can't fail. Here's the playbook.

Step 1 — Name the edge cases yourself

Agents test the happy path enthusiastically and skip the cases that actually break in production. So you name them. For any function, walk this list out loud and demand a test for each that applies:

  • Empty. Empty string, empty array, empty object, zero rows.
  • Null / missing. A required field absent, an optional value undefined, a lookup that finds nothing.
  • Boundary. Zero, one, the max length, one past the max, negative where only positive is expected.
  • Error paths. The network call fails, the file doesn't exist, the input is malformed. Assert on the error, not just that "it didn't crash."
  • The unhappy real case. The duplicate, the expired token, the wrong currency, the concurrent write.

Naming them by name is the whole trick. "Test the login function" gets you one happy-path test. "Test login with: valid credentials, wrong password, unknown user, empty password, and a locked account" gets you a suite that means something.

Step 2 — Demand assertions that constrain behavior

A test earns its keep only if it fails when the code is wrong. So every test must assert a specific expected value or behavior, not just "no exception was thrown" and not "the result is truthy." The bad pattern:

// Worthless: passes for almost any implementation
expect(formatPrice(1099)).toBeTruthy();

The version that catches bugs:

// Constrains behavior: fails if the format is wrong
expect(formatPrice(1099)).toBe("$10.99");
expect(formatPrice(0)).toBe("$0.00");
expect(() => formatPrice(-1)).toThrow("negative amount");

Tell the agent explicitly: no assertion may be satisfied by an obviously wrong implementation. If a test would still pass when the function just returns its input, it's not a test.

👑
A test that can't fail is decoration. If it stays green when you deliberately break the code, delete it.

Step 3 — The mutation check

Here's the one move that catches always-pass tests instantly, and almost nobody does it: after the agent writes the tests, deliberately break the code and confirm the tests go red. Flip a comparison, return a constant, delete a branch. If the suite stays green, the tests are theater. This is a thirty-second sanity check that separates real coverage from the illusion of it. Have the agent do it and paste the failing output as proof, then revert the break.

Step 4 — Forbid weakened assertions

When a test fails because the code is genuinely buggy, an agent will sometimes "resolve" it by editing the test instead of the code. This is the most dangerous failure in the whole workflow, because it converts a caught bug into a hidden one. Make the rule explicit in the prompt: if a test fails, the default assumption is the code is wrong, not the test. Any change to an assertion must be justified in one sentence. Treat a quietly deleted assertion as a finding, not a cleanup — the same suspicion you'd apply in the AI code review workflow.

The copy-paste test-request prompt

Write tests for <function/module>. Follow these rules exactly.

1. COVER THESE CASES BY NAME (add any I missed, skip any N/A):
   - empty input (string/array/object as applicable)
   - null / missing required field
   - boundary: 0, 1, max, max+1, negative
   - error paths: assert on the actual error, not just "no throw"
   - the realistic unhappy case: <e.g. duplicate, expired, wrong type>

2. EVERY test asserts a SPECIFIC expected value or behavior.
   No toBeTruthy, no "does not throw" as the only check. A test
   must fail if the implementation is obviously wrong.

3. MUTATION CHECK. After writing the tests, deliberately break the
   code (flip a comparison / return a constant). Run the tests and
   paste output proving the relevant tests now FAIL. Then revert.

4. If a test fails, assume the CODE is wrong. Do not weaken or
   delete an assertion to make it pass. If you change an assertion,
   justify it in one sentence.

Run the suite and paste the real final output.

What to test, and what not to

Test heavilyTest lightlyPayoff
Business logic, calculations, parsing, validationThin controllers, config wiringHigh
Error handling and edge casesFramework internals you don't ownHigh
Anything that touches money or authGetters, trivial passthroughsCritical

Chasing 100% coverage tempts agents to write low-value tests over trivial code just to move a number. Aim your effort at logic that would actually hurt if it broke.

Picking a model

Test writing is mostly a value job — Sonnet 5 or GPT-5.3 Codex churn out well-structured suites cheaply, and Gemini Flash's speed shines when you're iterating over dozens of cases. Save your strongest model (Opus 4.8, or Fable 5 on our benchmarks) for reasoning out the tricky invariants in security-critical or concurrency-heavy code.

Tests are the foundation everything else rests on: fixing bugs with AI depends on a reproduction that a test can encode, and migrating code safely is only possible when a green suite tells you nothing broke. In The Vibe Father, the AutoVibe gate runs your real suite before any change is accepted — but the discipline above works in any harness with a terminal and the will to break your own code on purpose.

Run every AI coding tool. Keep every conversation. Own your work.

The Vibe Father is the model-agnostic command deck we built for ourselves — 22 CLIs, multi-agent teams, your own keys.

Keep reading