← back to section

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_*.py file, test_* function, plain assert inside.
  • 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.parametrize replaces ten similar tests with one table.
  • A bad test depends on order, checks everything, checks nothing, or sleeps.