← 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 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_*.py file, test_* function, plain assert inside.
  • 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 in conftest.py; @pytest.mark.parametrize replaces ten similar tests with one.
  • A test without assert is always green; a bad test also depends on order, checks everything at once or sleeps instead of waiting.