“I ran it and it worked” is a claim about one input, on one machine, at one moment, made by the person least able to be objective about it. A test is the same claim written down so that a machine can check it, so that a stranger can see what you actually promised, and so that it can be checked again next week after somebody else changes something.
That last part is the reason tests exist. You are on a team now. Somebody will edit your function.
Start with assert
The simplest test in Python is a sentence that must be true. Here is a function from a booking program, exactly as it was inherited:
def spaces_left(capacity, booked):
"""Return how many spaces remain in a session."""
return capacity - bookedAnd here are three claims about it, in a separate file:
from bookings import spaces_left
assert spaces_left(10, 3) == 7
assert spaces_left(10, 10) == 0
assert spaces_left(10, 12) == 0
print("All checks passed.")Run it. The third claim is a lie:
Traceback (most recent call last):
File "/home/student/check_bookings.py", line 5, in <module>
assert spaces_left(10, 12) == 0
^^^^^^^^^^^^^^^^^^^^^^^^
AssertionErrorAn assert that holds does nothing at all — silence is a pass. An
assert that fails stops the program and names the line. Two lines
of setup, and you have found a real bug: an overbooked session
reports a negative number of free spaces, which will eventually be
printed to somebody as “-2 spaces available”.
What
assertwill not tell you
AssertionErroron its own does not say what the value was. For quick checks that is fine. The moment you want the actual number in the message, move tounittest.
unittest, for tests you keep
Once tests are worth keeping, they need names, and they need to all
run even when one fails. Python’s standard library has that built in.
Save this as test_bookings.py:
import unittest
from bookings import spaces_left
class TestSpacesLeft(unittest.TestCase):
def test_ordinary_session(self):
self.assertEqual(spaces_left(10, 3), 7)
def test_exactly_full(self):
self.assertEqual(spaces_left(10, 10), 0)
def test_overbooked_never_goes_negative(self):
self.assertEqual(spaces_left(10, 12), 0)
if __name__ == "__main__":
unittest.main()Three rules and you can write these: the file name starts with
test_, the class inherits from unittest.TestCase, and every
method whose name starts with test_ is a separate test. Run it
with python3 -m unittest test_bookings -v:
test_exactly_full (test_bookings.TestSpacesLeft.test_exactly_full) ... ok
test_ordinary_session (test_bookings.TestSpacesLeft.test_ordinary_session) ... ok
test_overbooked_never_goes_negative (test_bookings.TestSpacesLeft.test_overbooked_never_goes_negative) ... FAIL
======================================================================
FAIL: test_overbooked_never_goes_negative (test_bookings.TestSpacesLeft.test_overbooked_never_goes_negative)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/student/test_bookings.py", line 15, in test_overbooked_never_goes_negative
self.assertEqual(spaces_left(10, 12), 0)
AssertionError: -2 != 0
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)Look at the difference unittest bought. The two good tests still
ran. The failure has a name that says what was expected, and the
message is -2 != 0 — the actual value, not just “something was
false”. You now know the bug without opening the program.
The fix, and the proof
def spaces_left(capacity, booked):
"""Return how many spaces remain, never fewer than zero."""
remaining = capacity - booked
if remaining < 0:
remaining = 0
return remainingRun the same tests again, unchanged:
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OKThree dots, one per test, and OK. Notice the order of operations:
the test failed first, then the code changed, then the test passed.
A test you write after the fix, and that has never once failed, has
proved nothing — it might be testing nothing at all. Watch every new
test fail at least once.
Regression testing, honestly
A regression is something that used to work and stopped. The test
you just wrote is now permanent: every time anyone on your team
changes bookings.py, that overbooked case gets checked again for
free, forever, without anyone remembering it exists. That is the
entire deal, and it is why the tests are worth more than the hour
they cost.
Be honest about the limits, though:
- Passing tests do not mean the program is correct. They mean the cases you thought of behave as you expected. Testing can show the presence of bugs, never their absence.
- The bugs that survive are the ones nobody imagined. Which is why the most productive thing you can do is have a teammate write tests for your function. They will try things you consider absurd, and one of them will fail.
- A test that is wrong is worse than no test. It gives false confidence and eventually gets “fixed” by changing the code to match. Read a failing test carefully before assuming the code is the guilty party.
- Tests are code, so they need to be readable and they need
maintaining.
test_overbooked_never_goes_negativesays what it protects;test_3says nothing to the person who inherits it.
What to test, when you have ten minutes
Not everything. Start here, in this order:
- The ordinary case, so you know the function works at all.
- The boundaries — zero, empty, one item, exactly full, the last position in the list. Almost every off-by-one in Spot the Bug dies here.
- The case that already broke once. Every bug you fix earns a test, on the day you fix it. That is the cheapest test you will ever write and the one most likely to save you.
- What you promised in the docstring. If it says “never fewer than zero”, there should be a test with that name.
Write the test the bug reporter would have written
When your community partner says “it did something strange when the session was full”, the first move is not to open the code. It is to write a failing test that reproduces what they described. Now the complaint is a fact, the fix has a finish line, and the thing they noticed can never quietly come back.
The idea behind all of this, including what regression means for a project after you have gone, is Testing and Regression. When a test tells you something is wrong but not why, the next page is Reading a Traceback in Someone Else’s Code.
Curriculum connection
A2.3
demonstrate the ability to modify existing modular program code to enhance the functionality of a program.
Link to original
A4.2
develop and implement a formal testing plan (e.g., unit testing, integration testing, regression testing) for a software project to ensure program correctness;
Link to original
C2.1
demonstrate the ability to analyse a precondition (i.e., starting state) and a postcondition (i.e., ending state) in an algorithm;
Link to original