← назад к разделу

Всё, что нужно для теста, у вас уже есть: данные, проверки, assert. Осталось оформить это так, как принято в проектах, — чтобы тесты запускались одной командой, а отчёт показывал, что упало. Этим занимается pytest.

pytest находит файлы test_*.py и функции test_* ✓ test_товар_отдаётся_по_id ✓ test_страница_товаров ✓ test_заказ_без_токена ✗ test_итог_равен_сумме_позиций 3 прошли, 1 упаложидали 250, получили 240 имя теста — утверждение о поведении: по отчёту видно, что сломалось, ещё до того, как открыли код

Прогон показывает каждый тест отдельно, а по упавшему сразу видно, какое поведение сломалось.

Как выглядит тест

живой пример

def test_сумма_заказа_считается_по_позициям():
    позиции = [{"qty": 2, "price": 100}, {"qty": 1, "price": 50}]

    итого = sum(п["qty"] * п["price"] for п in позиции)

    assert итого == 250


test_сумма_заказа_считается_по_позициям()     # pytest вызовет её сам, здесь — вручную
print("тест прошёл")
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →

Никакой обёртки: обычная функция с именем, начинающимся на test_, внутри — обычный assert. Файл называется test_orders.py — тоже с приставкой test_. По этим двум приметам pytest и находит тесты.

Запуск — одной командой из корня проекта:

pytest
pytest test_orders.py          # один файл
pytest -k сумма                # только тесты, в имени которых есть «сумма»
pytest -v                      # с именами каждого теста

Отчёт показывает точку за пройденный тест, букву F за упавший, а для упавших — строку с assert, значения и сообщение.

Три части теста

У хорошего теста видно три части, и их обычно разделяют пустой строкой:

живой пример

def test_отменённый_заказ_не_оплачивается():
    заказ = {"id": "ord-042", "status": "CANCELLED"}       # подготовка

    можно = заказ["status"] in ("NEW", "PENDING_PAYMENT")  # действие

    assert можно is False                                  # проверка


test_отменённый_заказ_не_оплачивается()
print("тест прошёл")
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →

Подготовка — что имеем; действие — что делаем; проверка — чего ждём. Тест, где эти части перемешаны, читается тяжело и чинится ещё тяжелее.

Имя теста — это предложение

test_1, test_orders, test_ok не говорят ничего. Имя должно читаться как утверждение о поведении:

живой пример

def test_заказ_без_токена_не_создаётся(): ...
def test_итог_равен_сумме_позиций(): ...
def test_отмена_возвращает_деньги_в_течение_суток(): ...
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Неделя бесплатно →

Когда такой тест падает, отчёт сам объясняет проблему — до чтения кода.

Фикстура: подготовка, которую не копируют

Помните функцию войти из статьи про функции? В pytest такие вещи оформляют фикстурой:

import pytest

@pytest.fixture
def заголовки(client):
    ответ = client.post("/auth/login", json={"email": "anna@example.com", "password": "secret123"})
    return {"Authorization": "Bearer " + ответ.json()["token"]}


def test_создание_заказа(client, заголовки):
    ответ = client.post("/orders", json={"productId": "p-01", "quantity": 1}, headers=заголовки)

    assert ответ.status_code == 201

Механика простая: @pytest.fixture помечает функцию как подготовку, а тест получает её результат, просто назвав параметр тем же именем. pytest сам вызовет фикстуру перед тестом.

Общие фикстуры кладут в файл conftest.py рядом с тестами — оттуда они видны всем тестам в папке, импортировать ничего не нужно.

Параметризация: один тест вместо десяти

Когда отличаются только данные, копировать тест не нужно:

@pytest.mark.parametrize("статус, ожидаем", [
    ("NEW", True),
    ("PENDING_PAYMENT", True),
    ("SHIPPED", False),
    ("CANCELLED", False),
])
def test_можно_ли_отменить(статус, ожидаем):
    assert можно_отменить({"status": статус}) is ожидаем

pytest выполнит тест четыре раза и покажет их в отчёте по отдельности — видно, какой именно случай упал. Добавить пятый статус — одна строка в списке.

Что делает тест плохим

Зависимость от порядка. Тест, который работает только после другого, сломается, как только их запустят порознь или в другом порядке. Каждый тест готовит себе данные сам.

Проверка всего подряд. Тест на создание заказа не должен заодно проверять формат почты покупателя: упадёт — и непонятно, что сломалось.

Отсутствие проверки. Тест, который выполняет действия и ни разу не говорит assert, зелёный всегда. Это самый опасный вид — он создаёт ощущение покрытия.

Пауза вместо ожидания. time.sleep(5) в тесте — почти всегда признак того, что ожидание условия заменили надеждой.

Что дальше

В нашем тренажёре автотестов API всё это уже работает: вы пишете def test_…(client) с обычным assert, тест запускается по кнопке, а рядом крутится игрушечный сервис магазина. Плюс к зачёту ваши тесты прогоняют против сломанных версий сервиса — и зачёт ставится, только если тест эту поломку заметил.

Коротко

  • pytest находит тесты по приметам: файл test_*.py, функция test_*, внутри обычный assert.
  • В тесте видны три части: подготовка, действие, проверка.
  • Имя теста — утверждение о поведении; по нему должно быть понятно, что сломалось.
  • Фикстура (@pytest.fixture) — подготовка, которую не копируют; общие лежат в conftest.py.
  • @pytest.mark.parametrize заменяет десять похожих тестов одним с таблицей данных.
  • Плохой тест: зависит от порядка, проверяет всё подряд, не проверяет ничего или спит вместо ожидания.

Что почитать дальше