Reproduce the failure and keep its evidence
When a unit test fails, resist the urge to edit the function named in the failure immediately. First, run the test again without changing anything. Note the test name, the inputs, the expected value, the actual value, and any error message. A reliable repeat run gives you a starting point; an intermittent failure calls for a different investigation, such as checking shared state or order-dependent behavior.
Here is a small example you can try with Python 3.10 or newer. It uses only the standard-library unittest module. Save these lines in a file named test_prices.py: import unittest def final_price(price, discount): return price - discount - 1 class PriceTests(unittest.TestCase): def test_discount(self): self.assertEqual(final_price(10, 2), 8) if __name__ == "__main__": unittest.main()
From the directory containing the file, run python -m unittest -v test_prices.py. On a system where Python is invoked as python3, use python3 instead. The test expects 8, while the function returns 7. Keep the file unchanged for now: its failure gives you a simple case for practicing the investigation.
Read the failure as a comparison, not a diagnosis
An assertion failure reports a disagreement between what the test expected and what the code produced. It does not, by itself, prove that the production code is wrong. The test could contain an outdated expectation, the function could have a defect, or the caller could be supplying a value in an unexpected format. An exception provides a different clue: check where it was raised and what operation failed before assuming the last line shown is the root cause.
For the price example, write down the smallest useful account of the failure: final_price receives 10 and 2; the test expects 8; the result is 7. Then follow the calculation. Subtracting the discount gives 8, and the additional subtraction gives 7. You have found the operation that explains the difference, but you have not yet established whether that operation should be removed.
If a real failure produces a long traceback, start with the failing assertion and the first relevant line in your own code. Follow the values through the call rather than trying to understand every line of test-runner output. If the failure happens only when you run the whole suite, record that too. Another test may be leaving behind state that this test depends on.
Check the expected behavior at the boundary
Before making a change, ask what the function promises to do. Look at its name, parameters, nearby documentation, and other tests. Does discount mean an amount to subtract, or a percentage? Is there a fee that the test forgot to include? Does the function accept negative values? These questions help separate the observed calculation from the intended behavior.
In the example, the test treats final_price(10, 2) as a simple subtraction and expects 8. If that matches the function’s stated purpose, the extra minus 1 is likely the defect. If the function is supposed to apply a one-unit fee, the test or the function name may need correction instead. When the intended behavior is unclear, clarify it with the project’s requirements or maintainer rather than adjusting the assertion merely to make the test pass.
Also inspect the test’s setup. A test may fail because it uses the wrong input, depends on another test’s changes, or assumes a particular time, file, or environment setting. Check those dependencies before adding special cases to production code. A good test describes a meaningful behavior and supplies everything needed to exercise it predictably.
Make one focused change, then verify it
Once you understand both the result and the intended behavior, change the smallest appropriate piece. For the example, if the contract is simply to subtract a discount amount, change the return expression to price - discount. Run the failing test again. Its passing result confirms that this particular input now agrees with the expectation; it does not yet show that every related case works.
Next, run the surrounding test suite and consider one or two useful boundary cases, such as a zero discount or a discount equal to the price. Add a test when it captures intended behavior that was previously missing. Avoid adding assertions for arbitrary outputs just because the current implementation produces them.
Finish by reviewing the change alongside the original failure. Can you explain why the test failed, why the chosen code or test change is correct, and what the follow-up tests cover? If not, keep investigating. The goal is not merely a green test result; it is a clear reason to trust that the behavior now matches its intended contract.



