I Donβt Trust AI-Written Tests Until They Fail for the Right Reason
A small way to check whether your tests protect the requirement or simply agree with the code in front of them.
Your coding assistant finishes the function, writes the tests, runs them, and hands you a lovely green result. Honestly, that is a very tempting moment to close the tab and feel productive. But I want to know what would make those tests turn red.
If the implementation misunderstood the requirement and the tests repeated that misunderstanding, they can agree with each other beautifully. Meanwhile, the user still gets the wrong result. A second file is not automatically a second opinion.
This is not a reason to stop asking AI for tests. It is a reason to give those tests something specific to disagree with. Before trusting a passing suite, deliberately introduce one small, meaningful mistake and check whether the right assertion catches it. Then restore the correct code and run the tests again.
Let's make that less abstract with a shipping calculation small enough to check without trusting anyone's confident explanation, including mine.
Start with the rule before reading the implementation
For this original example, imagine a shop with a deliberately simple shipping policy. Orders below 5,000 cents pay 499 cents for shipping. Orders at or above 5,000 cents receive free shipping. Negative subtotals are invalid and must raise ValueError.
Assume the input is already an integer number of cents. Currency conversion, discounts, taxes, and input parsing are outside this little function's job.
Notice that the rule gives us an expected answer at exactly 5,000 cents. Free shipping starts there, not one cent later. I prefer writing that down before looking at the generated code because it keeps the code from quietly becoming the specification. If the policy is unclear, resolve the ambiguity first; no testing trick can decide what the business meant.
Here is a correct implementation for the stated rule.
def shipping_cents ( subtotal_cents ) : if subtotal_cents < 0 : raise ValueError ( 'subtotal must be nonnegative' ) return 0 if subtotal_cents >= 5000 else 499
Now imagine the proposed tests check that a 1,000-cent order pays 499 cents and a 6,000-cent order pays zero. Both pass, as they should. Those tests are useful, but they leave a particularly interesting customer standing between them.
Change one character and watch what stays green
In a disposable local copy, change the final comparison from >= 5000 to > 5000. Leave everything else alone.
The 1,000 cent test still passes. So does the 6,000-cent test. Neither input asks the function what happens exactly at the threshold. But a customer spending 5,000 cents now pays 499 cents when the policy promises free shipping. We introduced a real behavior change, and the original pair of tests did not object.
This is the part I find useful. We have moved from vaguely wanting better tests to naming an exact mistake the tests currently allow.
Add an assertion whose expected result comes from the policy.
assert shipping_cents ( 5000 ) == 0
Run it while the incorrect comparison is still present. It should fail because the returned fee is 499 instead of zero. Then restore >= and confirm that the same assertion passes.
A syntax error, broken import, or missing dependency would also produce a red run, but none of those would show that our boundary assertion caught the shipping bug. Read the failure, not just its color.
One caught bug is evidence, not a certificate
Now try a different intentional mistake, again starting from the restored correct implementation. Remove the negative-subtotal guard. The original two tests still pass. Even the new 5,000-cent test passes. A boundary test for free shipping does not automatically protect invalid-input behavior.
So add a separate check that a negative subtotal raises the promised exception. Here is a compact version using Python's standard test library, alongside the nearby valid values.
import unittest class ShippingTests ( unittest . TestCase ) : def test_fees ( self ) : for subtotal , expected in [ ( 0 , 499 ) , ( 4999 , 499 ) , ( 5000 , 0 ) , ( 5001 , 0 ) ] : with self . subTest ( subtotal = subtotal ) : self . assertEqual ( shipping_cents ( subtotal ) , expected ) def test_negative_subtotal ( self ) : with self . assertRaises ( ValueError ) : shipping_cents ( - 1 )
Put this below the function in a file named test_shipping.py, then run python -m unittest -v test_shipping. Keep the function and test class at the top level of the file.
The expected values are intentionally written out. Calculating them with another copy of the production comparison would risk reproducing the same mistake inside the test.
I checked these outcomes against the original and both altered versions. The original pair of tests misses both mistakes. The expanded checks catch the threshold change and the removed guard, while passing against the correct version. That is a result for this tiny fixture, not a claim about every AI assistant or a guarantee that the function has no other problems. Different mistakes need different evidence.
Give the assistant a review task with an observable finish
An instruction to add comprehensive tests sounds reassuring, but comprehensive is doing an awful lot of work there. Try a more concrete brief instead. Give the assistant the requirement, ask it to list expected outcomes before inspecting the implementation, and ask which plausible incorrect implementations each test would reject.
For the shipping example, the review brief can be this small.
- Derive the expected fees at 4,999, 5,000, and 5,001 cents from the written policy.
- Check that a negative subtotal raises ValueError without depending on the exact wording of the message.
- Change only the threshold comparison and identify the assertion that fails because of it.
- Restore the function, remove only the negative-input guard, and identify the exception check that fails.
- Restore the correct version again and show the final passing test output.
You still need to inspect the proposed expectations and the actual result. An assistant saying it verified the change is not the same as seeing which command ran, which test failed, and why.
For a bug fix, this can be even simpler. Run the regression test against the known buggy version and the fixed version. It should expose the intended bug in the first and pass in the second. Make sure it fails for the behavior you meant to fix, rather than for an unrelated incompatibility between versions.
Keep the experiment small enough to understand
This approach is closely related to mutation testing, where tools deliberately alter code and check whether tests detect the changes. PIT's explanation also makes an important distinction between a detected mutation, a surviving mutation, and code that no test reached. Some mutations do not change the behavior that matters, so a survivor is a reason to investigate rather than an automatic demand for another test. Chasing a perfect score can become a very elaborate way to avoid thinking about the requirement.
You do not need to install a mutation-testing tool to try the manual version on one small function. Tooling becomes more useful when you want to repeat the process across more code; the small example is about learning what evidence to look for.
Keep each fault isolated. Start from a passing baseline, make one change, inspect the result, and restore the original before trying the next one. Use a local practice copy or a disposable branch so an intentionally broken version cannot slip into your actual work.
For a first session, choose one small function with a clear output. Check a boundary, an invalid input, and one meaningful alternative result. Stop once you can explain what each assertion protects; a growing test count is not the goal.
If you need a fresh starting point, choose a small problem from this practice question collection and write the expected outcomes before opening a solution. Then make one plausible mistake and see whether your own checks notice it.
Leave with a review note you can defend
For this example, the useful summary is that the original checks missed the exact threshold and negative-input behavior. The added assertions caught those two deliberate faults, and the restored implementation passed them. That says considerably more than tests added, all green.
In an interview, this gives you a concrete way to explain your verification. During a code review, it gives the next person a reason to trust the specific behavior you checked. Neither requires pretending that a handful of tests proves the entire system correct.
I still want AI to help with the repetitive parts. I just want the final green result to mean more than the implementation and its tests agreeing with each other. Give the tests a believable mistake to catch, and make them earn a little of that confidence.
~ Emily
Comments
No comments yet. Start the discussion.