Python Book · Chapter 12

Testing your code

Running your program and eyeballing the output works for ten lines. For anything bigger, you want tests: small programs that check your code automatically, every time you change it.

assert

assert checks that something is true, and stops with an AssertionError if it is not:

def is_even(n):
    return n % 2 == 0

assert is_even(4)
assert not is_even(7)
assert is_even(0)

If all three pass, nothing happens. That silence is the point: you only hear about problems.

pytest

pytest is the most popular Python testing tool. Install it once:

python -m pip install pytest

Put your code in one file and your tests in a file whose name starts with test_. Every function starting with test_ is a test:

# grades.py
def letter(score):
    if score >= 80:
        return "A"
    if score >= 70:
        return "B"
    if score >= 60:
        return "C"
    return "F"
# test_grades.py
from grades import letter

def test_top_grade():
    assert letter(95) == "A"

def test_boundaries():
    assert letter(80) == "A"
    assert letter(79) == "B"
    assert letter(60) == "C"
    assert letter(59) == "F"

Run every test in the folder with:

python -m pytest

pytest prints a dot for each passing test. When one fails, it shows exactly which values did not match.

Choosing test cases

Bugs hide at the edges, so test those first:

Testing for exceptions

If your function should raise an error on bad input, test that too:

import pytest

def average(scores):
    if not scores:
        raise ValueError("cannot average an empty list")
    return sum(scores) / len(scores)

def test_average_rejects_empty_list():
    with pytest.raises(ValueError):
        average([])

Floats need care

Because 0.1 + 0.2 is 0.30000000000000004, compare floats with a tolerance:

def test_average():
    assert average([0.1, 0.2]) == pytest.approx(0.15)

Write the test first

A useful habit is to write a failing test before you fix a bug or add a feature:

  1. Write a test that describes what the code should do. Run it and watch it fail.
  2. Write the simplest code that makes it pass.
  3. Tidy the code up, re-running the tests to make sure nothing broke.

This is called test-driven development. Even if you do not follow it strictly, step 1 is valuable whenever you fix a bug: the test makes sure that bug never quietly comes back. (The regreSSHion brief shows what can happen without one.)

Exercises

1. Test a palindrome checker

Write pytest tests for an is_palindrome(s) function that ignores case. Cover at least four kinds of input.

Show answer
from palindrome import is_palindrome

def test_simple_palindrome():
    assert is_palindrome("level")

def test_ignores_case():
    assert is_palindrome("Racecar")

def test_not_a_palindrome():
    assert not is_palindrome("python")

def test_edge_cases():
    assert is_palindrome("")
    assert is_palindrome("a")

2. Find the bug with a test

This function should return the largest number in a list. Write a test that fails, then fix the function.

def largest(nums):
    best = 0
    for n in nums:
        if n > best:
            best = n
    return best
Show answer
def test_all_negative():
    assert largest([-5, -2, -9]) == -2   # fails: returns 0

Starting best at 0 is wrong when every number is negative. Start at nums[0] instead, or use the built-in max. Choosing an unusual input, all negatives, is what exposed the bug.

3. Test an exception

Write a test checking that int("abc") raises a ValueError.

Show answer
import pytest

def test_int_rejects_letters():
    with pytest.raises(ValueError):
        int("abc")

Where next?

You have finished the book. You now know enough Python to solve every problem in the problem archive. Pick one a day, and when you solve one, try writing a few pytest tests for your answer too.