runtime
121 / 250
lesson 121 Testing & Code Quality · intermediate

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.

You'll be able to
  • Use assert condition, message to 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
The idea

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.

example-1.py · Three checks on a VAT calculation
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")
example-2.py · A check that finds a bug
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.

example-3.py · Named tests, run one by one
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 assert to validate input from users or files. Python run with -O skips every assert, so assert quantity > 0 in production code may silently do nothing. Validate input with if and raise 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 assert in front of it.
Review
  • A test calls code with an input whose answer you worked out by hand and asserts the result.
  • assert condition, message raises 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.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Check results with assert and test functions — Testing & Code Quality · Runtime