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

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

тест POST /orders сервисмагазина 201 + тело код ответа поля тела заголовки остаток на складе сервис сломали: валидация выключенахороший тест обязан покраснеть зелёный тест ничего не значит, пока не краснел на поломке

Тест шлёт запрос и проверяет четыре слоя, а качество самого теста доказывается только на сломанном сервисе.

Из чего состоит проект с тестами

tests/
  conftest.py          общая подготовка: клиент, вход, данные
  test_products.py     тесты каталога
  test_orders.py       тесты заказов
requirements.txt       список библиотек

Библиотек нужно ровно две: pytest запускает тесты, requests отправляет запросы.

pip install pytest requests

В conftest.py держат подготовку, которая нужна каждому тесту: клиента с адресом сервиса и вход под пользователем. Это фикстуры — как они работают, разобрано в статье про pytest.

import pytest
import requests

БАЗА = "https://shop.example.com/api"


@pytest.fixture
def client():
    return requests.Session()


@pytest.fixture
def заголовки(client):
    ответ = client.post(f"{БАЗА}/auth/login",
                        json={"email": "anna@example.com", "password": "secret123"})
    assert ответ.status_code == 200, "не удалось войти — тесты дальше бессмысленны"
    return {"Authorization": "Bearer " + ответ.json()["token"]}

Что проверять в ответе

Четыре слоя, от дешёвого к ценному.

Код ответа — 200, 201, 404, 422. Одна строка assert ответ.status_code == 201, и она есть в каждом тесте.

Значимые поля тела. Вот здесь ошибаются чаще всего: сравнивают тело ответа целиком и привязывают тест к каждому полю, какое сервис вернёт.

живой пример

# ошибка: так не надо — сравнили тело целиком и упали из-за нового поля
тело = {"id": "ord-042", "status": "NEW", "total": 4990, "createdAt": "2026-03-01"}

assert тело == {"id": "ord-042", "status": "NEW", "total": 4990}
Запустить

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

живой пример

# так надо — проверяем поля, ради которых тест написан
тело = {"id": "ord-042", "status": "NEW", "total": 4990, "createdAt": "2026-03-01"}

assert тело["status"] == "NEW"
assert тело["total"] == 4990
print("проверили значимые поля")
Запустить

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

Сервис добавил createdAt и ничего не сломал, а первый тест покраснел на ровном месте.

Заголовки, на которые полагается клиент. Например Location у созданного объекта: по нему клиент идёт за результатом.

Последствия в системе. Самое ценное и чаще всего забытое: заказ создан — а остаток товара уменьшился? Ответ 201 об этом не говорит.

def test_остаток_уменьшается(client, заголовки):
    было = client.get(f"{БАЗА}/products/p-01").json()["inStock"]

    client.post(f"{БАЗА}/orders",
                json={"productId": "p-01", "quantity": 2}, headers=заголовки)

    стало = client.get(f"{БАЗА}/products/p-01").json()["inStock"]
    assert стало == было - 2

Проверять отказы не менее важно

Тесты только на счастливый путь пропускают половину дефектов. На каждый сценарий стоит задать три вопроса: что при неверных данных, что без прав, что при конфликте.

def test_заказ_без_токена(client):
    ответ = client.post(f"{БАЗА}/orders", json={"productId": "p-01", "quantity": 1})

    assert ответ.status_code == 401


def test_пустое_тело(client, заголовки):
    ответ = client.post(f"{БАЗА}/orders", json={}, headers=заголовки)

    assert ответ.status_code == 422
    поля = [д["field"] for д in ответ.json()["error"]["details"]]
    assert "productId" in поля

И отдельно — что отказ ничего не изменил: остаток на месте, заказ не создан. Отказ, который успел что-то поменять, — частая находка при проверке прав.

Данные готовят запросом, а не кликами

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

@pytest.fixture
def оплаченный_заказ(client, заголовки):
    return client.post(f"{БАЗА}/orders",
                       json={"productId": "p-01", "quantity": 1}, headers=заголовки).json()

И каждый тест готовит данные себе сам. Тесты, которые передают состояние друг другу, ломаются от смены порядка запуска и падают «по очереди» без причины.

Зелёный тест ничего не гарантирует

Тест — такой же код, и в нём тоже бывают ошибки. Тест, который ничего не проверяет, зелёный всегда.

# так не надо — запрос отправлен, а проверки нет
def test_заказ(client, заголовки):
    client.post(f"{БАЗА}/orders", json={"productId": "p-01", "quantity": 1}, headers=заголовки)
    assert True
# так надо — тест заканчивается проверкой ответа
def test_заказ(client, заголовки):
    ответ = client.post(f"{БАЗА}/orders", json={"productId": "p-01", "quantity": 1}, headers=заголовки)

    assert ответ.status_code == 201
    assert ответ.json()["status"] == "NEW"

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

В тренажёре автотестов API это происходит само: тесты прогоняются против десяти сломанных версий сервиса, и в отчёте видно, какие поломки они поймали, а какие прошли мимо. Зачёт ставится, только если тест хоть что-то ловит.

Коротко

  • Проекту нужны две библиотеки: pytest и requests; общая подготовка живёт в conftest.py.
  • Проверяют четыре слоя: код ответа, значимые поля, заголовки и последствия в системе.
  • Тело ответа не сравнивают целиком: новое поле роняет такой тест без причины.
  • Отказы проверяют наравне со счастливым путём — и отдельно то, что отказ ничего не изменил.
  • Зелёный тест ничего не гарантирует, пока не показал, что краснеет на сломанном сервисе.

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