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 does.
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
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →
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
Three parts of a test
A good test shows three parts, usually separated by blank lines: setup (what we have), action (what we do), check (what we expect). A test where those are mixed reads badly and is repaired worse.
The test name is a sentence
test_1, test_orders, test_ok say nothing. The name should read as a claim
about behaviour:
live example
def test_order_without_token_is_rejected(): ...
def test_total_equals_sum_of_items(): ...
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →
When such a test fails, the report explains the problem before you open the code.
A fixture: setup that is not copied
import pytest
@pytest.fixture
def headers(client):
response = client.post("/auth/login", json={"email": "anna@example.com", "password": "secret123"})
return {"Authorization": "Bearer " + response.json()["token"]}
def test_order_is_created(client, headers):
response = client.post("/orders", json={"productId": "p-01", "quantity": 1}, headers=headers)
assert response.status_code == 201
@pytest.fixture marks a function as setup, and a test receives its result
simply by naming a parameter the same way. 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
@pytest.mark.parametrize("status, expected", [
("NEW", True),
("PENDING_PAYMENT", True),
("SHIPPED", False),
("CANCELLED", False),
])
def test_can_be_cancelled(status, expected):
assert can_cancel({"status": status}) is expected
pytest runs the test four times and shows them separately — you see which case failed. Adding a fifth status is one line.
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.
Checks nothing. A test that performs actions and never says assert is
always green. That is the most dangerous kind — it creates a feeling of coverage.
Sleeps instead of waiting. time.sleep(5) in a test almost always means
waiting for a condition was replaced by hope.
What is next
In our API autotest trainer
all of this already works: you write def test_…(client) with a plain assert,
the test runs on a button, and a toy shop service runs next to it. On top of that
your tests are run against broken versions of the service — and the task is
only credited if the test noticed the breakage.
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; the report should say what broke.
- A fixture is setup that is not copied; shared ones live in
conftest.py. @pytest.mark.parametrizereplaces ten similar tests with one table.- A bad test depends on order, checks everything, checks nothing, or sleeps.
What to read next
- API autotests in Python — putting it together on a real service.
- Working with data — what exactly to check in responses.