Check results with assert and test functions
A test calls your code with an input whose answer you already know and checks the result. assert does the checking; a test_ function gives each check a name you can run again after every change.
- Use
assert condition, messageto check a result, and read the AssertionError when it fails - Put related checks in
test_functions so each one has a name and can be run on its own - Know when assert is the wrong tool: checking user input that must always be validated
You fix a shipping rule today; next month someone changes the discount code and the shipping rule breaks again without anyone noticing. A test prevents that: a small piece of code that calls a function with an input whose correct answer you worked out by hand, and complains if the answer is different. Tests are run again after every change, so a bug that comes back is caught at once.
The simplest check is the assert statement: assert condition. If the condition is true nothing happens; if it is false, Python raises AssertionError and stops. assert condition, message adds a message to the error, which is worth writing: it is the first thing you read when the check fails.
Run each example to see what it prints. Running examples doesn't count toward progress.
def price_with_vat(net, rate): return round(net * (1 + rate), 2) assert price_with_vat(10.00, 0.20) == 12.0 assert price_with_vat(19.99, 0.20) == 23.99 assert price_with_vat(0, 0.20) == 0 print("3 checks passed")
def shipping_cost(total): if total > 50: return 0 return 4.99 assert shipping_cost(20) == 4.99 assert shipping_cost(50) == 0, f"an order of exactly 50 should ship free, got {shipping_cost(50)}" print("all checks passed")
A file of loose asserts stops at the first failure, and the error doesn't say *which* rule broke. The usual shape is a test function: a function whose name starts with test_ and describes one behaviour, containing the asserts for it. A test runner such as pytest (on your own computer: pytest in a terminal) finds every test_ function, runs each one, and reports which passed and which failed, so one failure doesn't hide the others.
The loop below does what a runner does, so you can see it happen: call each test, and report AssertionError as a failure instead of stopping.
def shipping_cost(total): if total > 50: return 0 return 4.99 def test_small_order_pays_shipping(): assert shipping_cost(20) == 4.99 def test_free_from_exactly_50(): assert shipping_cost(50) == 0, f"got {shipping_cost(50)}" def test_large_order_ships_free(): assert shipping_cost(120.5) == 0 for test in [test_small_order_pays_shipping, test_free_from_exactly_50, test_large_order_ships_free]: try: test() print("PASS", test.__name__) except AssertionError as err: print("FAIL", test.__name__, "-", err)
When to use it: for every function whose answer you can work out by hand. Check ordinary inputs, and especially the boundaries where rules change (exactly 50 here).
Mistakes to avoid:
- Using
assertto validate input from users or files. Python run with-Oskips every assert, soassert quantity > 0in production code may silently do nothing. Validate input withifandraise ValueError; keep assert for tests and for facts about your own code. assert (total == 12, "msg")with brackets: that asserts a two-item tuple, which is always true, so it can never fail (Python warns about it).- Comparing floats exactly when the arithmetic isn't exact: round first, or compare with a tolerance (the next lesson's
assertAlmostEqual). - Tests that can't fail, for example a comparison with no
assertin front of it.
- A test calls code with an input whose answer you worked out by hand and asserts the result.
assert condition, messageraises AssertionError with your message when the condition is false.test_functions give every check a name; a runner runs them all and reports each failure separately.- Validate real input with
raise, not assert. Next: Python's built-in test framework, unittest.