Section 1: pytest essentials
Your first tests and reading failures
pytest finds tests by convention. Files named test_*.py or *_test.py, functions named test_*, and classes named Test* without an __init__ method. Create tests/test_totals.py:
from billable.totals import line_total, invoice_total
def test_line_total_multiplies_quantity_and_price():
assert line_total(quantity=3, unit_price_cents=250) == 750
def test_invoice_total_sums_lines():
lines = [(1, 1000), (2, 500)]
assert invoice_total(lines) == 2000
Run the suite from the project root:
pytest
Each dot in the output is a passing test. An F is a failure, an E is an error during setup.
Reading a failure
Change the expected value in the second test to 2100 and run again. pytest shows the failing line, the values on both sides of the comparison and, for collections, a diff of what differs. This is the assertion rewriting feature: you write a plain assert, and pytest explains the failure in detail without special methods like assertEqual.
Useful command line options
pytest -xstops at the first failure.pytest -k discountruns only tests whose names match "discount".pytest tests/test_totals.py::test_invoice_total_sums_linesruns one test.pytest -qgives quieter output,-vmore detailed output.pytest --lfreruns only the tests that failed last time.
Name tests as sentences
A test name like test_total tells you nothing when it fails. test_invoice_total_sums_lines describes the behaviour, so a failing test name in a CI log already tells you what broke.
Arrange, act, assert
Structure each test in three parts: set up the inputs, call the code, check the result. Keep one behaviour per test. Several asserts are fine if they check the same behaviour, but if a test needs a long name with "and" in it, split it.
Configuration
Add a [tool.pytest.ini_options] section to pyproject.toml with testpaths = ["tests"] and addopts = "-ra". The -ra flag prints a short summary of skipped and failed tests at the end.
Write three more tests for line_total, including a quantity of zero. One of them should fail. That is our first bug.