You already have everything a test needs: data, checks, assert. What is left is
packaging it the way projects do — so tests run with one command and the report
shows what failed. That is what pytest is for: it finds the tests in your files
and runs them.
What a test looks like
live example
def test_order_total_is_sum_of_items():
items = [{"qty": 2, "price": 100}, {"qty": 1, "price": 50}]
total = sum(i["qty"] * i["price"] for i in items)
assert total == 250
test_order_total_is_sum_of_items() # pytest calls it itself; here we call it by hand
print("test passed")
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
No wrapper: an ordinary function whose name starts with test_, an ordinary
assert inside. The file is called test_orders.py — also with the test_
prefix. Those two marks are how pytest finds tests.
Run it with one command from the project root:
pytest
pytest test_orders.py # one file
pytest -k total # only tests with "total" in the name
pytest -v # with every test name
The report prints a dot for a passing test, an F for a failing one and the
assert line with the expected and the actual value.
Three parts of a test
A test shows three parts, separated by blank lines: setup — what we have, action — what we do, check — what we expect.
live example
def test_cancelled_order_is_not_paid():
order = {"id": "ord-042", "status": "CANCELLED"} # setup
payable = order["status"] in ("NEW", "PENDING_PAYMENT") # action
assert payable is False # check
test_cancelled_order_is_not_paid()
print("test passed")
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
A test where those parts are mixed is hard to read and harder to fix.
The test name is a sentence
test_1, test_orders, test_ok say nothing. The name reads as a claim about
behaviour — test_total_equals_sum_of_items, test_cancelled_order_is_not_paid,
test_empty_cart_is_not_submitted. Then a failing test tells you what broke
before you open the code.
A fixture: setup that is not copied
The same setup is not copied into every test but moved into a fixture — a
function marked @pytest.fixture:
import pytest
@pytest.fixture
def order():
return {"id": "ord-042", "status": "NEW",
"items": [{"qty": 2, "price": 100}]}
def test_total_equals_sum_of_items(order):
total = sum(i["qty"] * i["price"] for i in order["items"])
assert total == 200
A test receives the fixture result simply by naming a parameter after it — pytest
calls it before the test. Shared fixtures live in conftest.py next to the
tests: visible to every test in the folder, no imports needed.
Parametrisation: one test instead of ten
When only the data differs, there is no need to copy the test:
import pytest
@pytest.mark.parametrize("status, expected", [
("NEW", True),
("SHIPPED", False),
("CANCELLED", False),
])
def test_can_be_cancelled(status, expected):
assert (status == "NEW") is expected
pytest runs the test three times and shows the cases separately — you see which one failed. Adding another status is one line.
A test that checks nothing
The most dangerous test is the one that performs actions and never says
assert: it is green always.
live example
# wrong way — no check, the test is green whatever happens
def test_order_is_paid():
order = {"status": "NEW"}
order["status"] = "PAID"
test_order_is_paid()
print("passed without checking anything")
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
live example
# right way — the test ends with a check
def test_order_is_paid():
order = {"status": "NEW"}
order["status"] = "PAID"
assert order["status"] == "PAID"
test_order_is_paid()
print("passed and really checked")
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
One line of difference, and the meaning is the opposite: the first test cannot fail, the second one notices the breakage.
What makes a test bad
Depends on order. A test that only works after another one breaks as soon as they run separately. Every test prepares its own data.
Checks everything at once. A test for order creation should not also validate the customer's email format: when it fails you cannot tell what broke.
Sleeps instead of waiting. time.sleep(5) in a test almost always means
waiting for a condition was replaced by hope.
Practice here: API autotests
In short
- pytest finds tests by their marks:
test_*.pyfile,test_*function, plainassertinside. - A test shows three parts: setup, action, check.
- The name is a claim about behaviour; a failing test says what broke.
- A fixture (
@pytest.fixture) is setup that is not copied, shared ones live inconftest.py;@pytest.mark.parametrizereplaces ten similar tests with one. - A test without
assertis always green; a bad test also depends on order, checks everything at once or sleeps instead of waiting.
What to read next
- API autotests in Python — putting it together on a real service.
- assert and errors — making a failing test readable.
- Functions — what
def, a parameter andreturndo. - Working with data — what exactly to check in responses.