Your First pytest
pytest is the testing tool that almost every Python project uses. It makes tests shorter to write, runs all of them with one command, and, when one fails, shows exactly what went wrong, including the value the function actually returned. It is a package, so it is installed into the project's virtual environment with pip, exactly like requests in Chapter 9.
Think of the difference between an inspector who writes "failed" on a form and one who hands you a note: expected 6, got 60, on this line. pytest is the second inspector.
Installing It
With the project's environment active, python -m pip install pytest installs it, together with the few small packages it needs. Add it to requirements.txt with pip freeze as usual, so anyone rebuilding the environment gets it too.
Test Files and Test Functions
pytest finds tests by their names. A test file is any file whose name starts with test_, and inside it, every function whose name starts with test_ is a test. Each test is an ordinary function containing ordinary assert lines; no special checking functions are needed. The file below keeps two of the last topic's checks as tests and adds a new one for bad input, each with a name that says what it promises.
import pytest from tips import calculate_tip def test_fifteen_percent(): assert calculate_tip(40, 15) == 6 def test_zero_percent_is_no_tip(): assert calculate_tip(40, 0) == 0 def test_percent_over_100_is_refused(): with pytest.raises(ValueError): calculate_tip(40, 250)
Running Them
python -m pytest, run in the project folder, finds every test file, runs every test, and prints a summary. Each passing test is a dot; the last line counts what passed.
(.venv) you@mac python-course % python -m pytest ============================= test session starts ============================== platform darwin -- Python 3.15.0, pytest-9.1.1, pluggy-1.6.0 rootdir: /Users/you/python-course collected 3 items test_tips.py ... [100%] ============================== 3 passed in 0.01s ===============================
Reading a Failure
Now make the same mistake as in the last topic: on the return line of calculate_tip, change the division by 100 to a division by 10, and run the tests again. The first dot becomes an F. Below the summary, pytest shows the failing test, marks the line with an arrow, and, on the lines starting with E, explains it: it asserted 60.0 equals 6, where 60.0 is what calculate_tip(40, 15) returned. That is the value a bare assert never showed.
(.venv) you@mac python-course % python -m pytest
============================= test session starts ==============================
platform darwin -- Python 3.15.0, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/you/python-course
collected 3 items
test_tips.py F.. [100%]
=================================== FAILURES ===================================
_____________________________ test_fifteen_percent _____________________________
def test_fifteen_percent():
> assert calculate_tip(40, 15) == 6
E assert 60.0 == 6
E + where 60.0 = calculate_tip(40, 15)
test_tips.py:7: AssertionError
=========================== short test summary info ============================
FAILED test_tips.py::test_fifteen_percent - assert 60.0 == 6
========================= 1 failed, 2 passed in 0.02s ==========================
Notice that the zero-percent test still passed. Zero times anything is zero, so that test could not see the mistake. It is one more reason to test several kinds of case, not just one.
Testing That an Error Is Raised
The third test checks the rule from Chapter 8: a percentage over 100 must be refused. with pytest.raises(ValueError): is a block that passes only if the code inside it raises a ValueError. If calculate_tip stopped checking its input and happily returned a tip, that test would fail, which is exactly what you want to know.
- "pytest needs special assertion functions." Plain assert works. pytest reads it and explains the failure for you.
- "Tests belong in the same file as the code." Keep them in separate files whose names start with test_, so pytest can find them and the program stays clean.
- "A test that has never failed is a good test." Break the code on purpose once and watch the test catch it. A test that cannot fail tests nothing.
- A clear failure message is most of the value of a test tool, and pytest's is one of the best.
- The same pytest skills carry straight into Backend Deep Dive and into almost every professional Python project.
Knowledge Check
How does pytest find the tests to run?
- By names: files and functions starting with test_
- By reading a list of tests kept in requirements.txt
- By running every Python file in the folder
- By asking you which tests to run each time it starts
In the failure report, what does E assert 60.0 == 6 tell you?
- The test expected 60.0 and the function returned 6
- The test file has a syntax error on that line
- The function returned 60.0 where the test expected 6
- pytest itself is broken and could not compare them
What does with pytest.raises(ValueError): check?
- That no error at all happens inside the block
- That the ValueError is caught and hidden from the user
- That the function's result is a ValueError object
- That the code inside the block raises a ValueError
Why did the zero-percent test pass even with the mistake in the function?
- pytest only runs the first test in a file when one fails
- Zero times anything is zero, so this test cannot see the mistake
- Tests with the word zero in their name are skipped
- The mistake only affects bills larger than 40
You got correct