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:
- Boundaries: 79 and 80 in the example above, the first and last items of a list.
- Empty input: an empty list, an empty string, zero.
- One item: a list with a single element.
- Unusual values: negative numbers, duplicates, very large inputs.
- The normal case: one or two ordinary examples.
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:
- Write a test that describes what the code should do. Run it and watch it fail.
- Write the simplest code that makes it pass.
- 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.