Before you start
- Python 3.8 or newer and pip
- A function you can call without a database or network (we write one)
A unit test runs a small piece of code with known input and checks the result. When you change the code later, the tests tell you within a second whether you broke something. pytest makes writing them close to effortless.
1. Install pytest
python -m pip install pytest
2. The code to test
Save this as pricing.py:
def discounted_price(price: float, percent: float) -> float:
"""Return price after a percentage discount, rounded to cents."""
if price < 0:
raise ValueError("price cannot be negative")
if not 0 <= percent <= 100:
raise ValueError("percent must be between 0 and 100")
return round(price * (1 - percent / 100), 2)
3. Write the tests
Save this as test_pricing.py in the same folder. pytest looks for files starting with test_, and functions starting with test_.
import pytest
from pricing import discounted_price
def test_ten_percent_off():
assert discounted_price(80.0, 10) == 72.0
def test_no_discount_returns_the_same_price():
assert discounted_price(19.99, 0) == 19.99
@pytest.mark.parametrize(
"price, percent, expected",
[
(100, 100, 0.0),
(59.99, 15, 50.99),
(0, 50, 0.0),
],
)
def test_several_cases(price, percent, expected):
assert discounted_price(price, percent) == expected
@pytest.mark.parametrize("price, percent", [(-1, 10), (10, -5), (10, 101)])
def test_bad_input_is_rejected(price, percent):
with pytest.raises(ValueError):
discounted_price(price, percent)
Three ideas are in that file:
- A plain
assert. pytest rewrites it so a failure shows you both sides, without special assertion methods. @pytest.mark.parametrize. One test function runs once per row, and each row is reported separately.pytest.raises. The test passes only if the code under test raises that exception. Checking that bad input is refused is as important as checking good input.
4. Run them
python -m pytest -q
........ [100%]
8 passed in 0.16s
Two plain tests plus three rows for each of the two parametrized tests make eight. Each dot is one passing test.
5. Read a failure
Change one expected value on purpose, from 50.99 to 51.00, and run again:
...F.... [100%]
================================== FAILURES ===================================
______________________ test_several_cases[59.99-15-51.0] ______________________
price = 59.99, percent = 15, expected = 51.0
> assert discounted_price(price, percent) == expected
E assert 50.99 == 51.0
E + where 50.99 = discounted_price(59.99, 15)
test_pricing.py:23: AssertionError
=========================== short test summary info ===========================
FAILED test_pricing.py::test_several_cases[59.99-15-51.0] - assert 50.99 == 51.0
1 failed, 7 passed in 0.15s
Read from the bottom. The summary names the failing test and the specific row (59.99-15-51.0). Above it, the line marked > is the failing assertion, the E lines show what the function really returned (50.99) and the expected value, and the where line shows the call that produced it. The other seven tests still passed, so you know the fault is confined to that case.
What to do next
- Run tests every time you save, or at least before every commit. Fast tests that run constantly catch more than slow tests that run rarely.
- Test behaviour, not implementation. Check what the function returns for given input, not how it works inside, so you can refactor without rewriting tests.
- Write a test for every bug you fix. The test fails before the fix and passes after, and the bug cannot quietly return.
- Run them in CI so that no change is merged without them. The GitHub Actions tutorial shows how.
Expected result
pytest reports 8 passed. After you change one expected value, it reports 1 failed with the value it got and the value you expected.
Frequently asked questions
Where should tests live?
In a tests folder next to your code, or beside the modules. pytest finds files named test_*.py or *_test.py and functions named test_*.
How many tests are enough?
Enough to make you confident changing the code. Cover normal cases, the edges (zero, empty, maximum) and the ways it should fail.
References
From Grokem: https://www.grokem.com/testing-and-qa/unit-testing/pytest-first-unit-test




