Read the function called next, prove the culprit
A fix whose unit test goes green can be undone, within the same execution turn, by the function called right after it, and the already fixed function only becomes a proven culprit once a test has isolated its effect from that of the functions that follow.
A fix that corrects exactly the faulty function, with a unit test that goes green, can change nothing the user actually sees. The reason comes down to one thing: the function you just fixed is almost never the last one to touch the value. The corrected result still passes, within the same execution turn, through one or more functions called right after it, and one of them can undo the fix without any test flagging it.
The fix that does not survive the next call
Take a case built to stand on its own, with no dependency on any project file. The function formatPrice used to round a price in cents incorrectly, the fix now makes it round to two decimal places. Its own test passes. The price displayed on screen is still wrong, though, because a function applied right after it, applyDiscount, turns that text back into a number and then rounds it down a second time.
function formatPrice(cents) {
return (cents / 100).toFixed(2); // fixed: exact two decimal places
}
function applyDiscount(priceText) {
const price = parseFloat(priceText);
return Math.floor(price * 0.9); // rounds again, downward
}
console.log(formatPrice(1299)); // 12.99, the fix works on its own
console.log(applyDiscount(formatPrice(1299))); // 11, the fix is undone
The first call proves that formatPrice is correct. The second call, the one that actually reaches the screen, proves the opposite. A fix is proven on that second figure, never on the first.
The plausible culprit is not the proven culprit
Faced with a display that is still wrong after a fix, the function already changed becomes the prime suspect again. That is a plausible culprit, not a proven one. To settle it, read the first and last instructions of every function called right after the one that has just been fixed, not just that function on its own. In the example, the last instruction of applyDiscount, Math.floor, is the line that wipes out the cents formatPrice had just restored.
The final proof is not read in the code, it is read in an output. Write a test that isolates exactly the input and the output visible on screen, in this case the value applyDiscount returns, and read what that test actually returns before naming a culprit. It is the same principle as checking the actual state before acting on a request: a fix is established on the output produced, never on a reading of the code supposed to produce it.
When the culprit stays undetermined
Two functions called in the same turn can each alter the same value in a way that, taken in isolation, produces a plausible result. A test that does not separate their effects settles nothing. In that case, the correct answer is not to pick a function at random to close the matter: it is to write two separate tests, one per suspect function, each isolating it from the other, and then to say that the culprit's identity stays undetermined until both tests have been run and read.
From the fix applied to a settled verdict
A developer fixes the function that rounds a displayed price, its unit test goes green, then he restarts the application and still sees a badly rounded price on screen.
Write, in one sentence, what this situation establishes, and in one sentence what it does not establish.
What this establishes: The fix applied to the rounding function was not enough to make the bad rounding disappear from the screen.
What this does not establish: It does not establish that the fixed function is still at fault, since the displayed price may have been altered again by a function called after it.
The three most common miscalibrations
- Too broad Every function fixed in this application produces a result that is still visibly wrong on screen.
- Too narrow This finding only concerns this one run of the application and will not recur if it is restarted a second time.
- Off target The rounding function's unit test now covers every price case the application can encounter.
- A fix whose unit test goes green guarantees nothing about the screen if a function called right after it still touches the same value.
- Reading only the function you just fixed leaves invisible everything that happens in the functions that follow within the same execution turn.
- A plausible culprit remains a hypothesis until a test has isolated its exact input and output.
- A fix is proven by the output a test returns, not by re-reading the code supposed to produce it.
- When two suspect functions each alter the same value in a plausible way, the honest answer is to say the culprit stays undetermined until two separate tests have told them apart.
Take a fix you applied recently without revalidating it with an isolated test, write a disposable function of a few lines that reproduces the exact chain between the fixed function and the one that calls it next, then run that test and read its output before continuing to trust the fix.