Pytest организует запуск и диагностику тестов, а Requests отправляет HTTP-запросы и предоставляет ответы для проверки. Вместе они дают простой стек для API-автоматизации на Python. Его сильная сторона — возможность явно описать подготовку, действие и ожидаемый бизнес-результат без браузерной прослойки.
Простота первого запроса не должна обманывать. Качественный API-тест проверяет права, состояние и последствия операции, а не только код 200. Ему нужны контролируемые данные, ограниченное ожидание и понятная информация о том, какое правило нарушено.
Подготовка проекта
Создайте отдельное виртуальное окружение поддерживаемой версии Python и установите pytest и requests. В рабочем проекте зависимости фиксируют выбранным менеджером и сохраняют lock-файл или согласованный requirements-файл. Адрес тестового API и credentials получают из конфигурации окружения.
Не зашивайте production-адрес в общий клиент. Удобно проверять разрешённое окружение до первой изменяющей операции. Секреты не должны попадать в имена параметров, отчёт или сообщения assert. Для примеров ниже используются только локальные синтетические данные без авторизации.
Самостоятельный локальный пример
Сохраните код как test_orders.py и выполните python -m pytest -v test_orders.py. Fixture поднимает собственный HTTP-сервер на свободном локальном порту и останавливает его после теста. Установленные pytest и requests — единственные внешние Python-зависимости.
import json
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import pytest
import requests
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
found = self.path == '/orders/42'
data = {'id': 42, 'status': 'draft'} if found else {'code': 'not_found'}
body = json.dumps(data).encode()
self.send_response(200 if found else 404)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
@pytest.fixture
def api_url():
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f'http://127.0.0.1:{server.server_port}'
finally:
server.shutdown()
server.server_close()
thread.join()
@pytest.mark.parametrize('path, status, expected', [
('/orders/42', 200, {'id': 42, 'status': 'draft'}),
('/orders/999', 404, {'code': 'not_found'}),
])
def test_order_read(api_url, path, status, expected):
response = requests.get(api_url + path, timeout=(2, 3))
assert response.status_code == status
assert response.headers['Content-Type'].startswith('application/json')
assert response.json() == expectedОжидаются два независимых успешных кейса: существующий заказ и отсутствующий ресурс. Это демонстрация клиента и fixtures на контролируемом сервере, а не проверка реального сервиса заказов. В продукте тест вызывает приложение, а ожидаемые значения выводятся из требований и подготовленных данных.
Почему нужен timeout
Requests без явно заданного timeout может ждать неопределённо долго. В примере кортеж задаёт ограничения подключения и ожидания чтения. Это не общий дедлайн всей загрузки: ответ, постепенно передающий данные, имеет другую временную семантику.
Выбирайте значения по условиям тестового окружения и отличайте сетевой отказ от HTTP-ошибки. ConnectionError означает проблему получения ответа, а 403 — полученный ответ с запретом. Их нельзя автоматически считать одним дефектом авторизации или одинаково скрывать общим except.
Статус, тело и эффект
Проверяйте согласованный статус, структуру, существенные значения и изменение состояния. После создания заказа полезно прочитать его и убедиться, что владелец и сумма правильные. Одного наличия id недостаточно: сервер мог сохранить неверные данные или вернуть запись другого пользователя.
response.json() успешно разбирает JSON, но не доказывает успешность операции. Для ожидаемой ошибки нельзя бездумно вызывать raise_for_status до проверки её контракта: исключение прервёт содержательное assertion. Положительные и отрицательные сценарии должны явно задавать допустимый исход.
Для асинхронного API отличайте принятие запроса от завершения работы. Ответ 202 может означать создание операции, состояние которой читают по отдельному идентификатору. Опрос должен иметь дедлайн и проверять терминальный статус, не создавая новую операцию при каждой попытке. При таймауте сохраняйте последнее состояние и время наблюдения. Это даёт более содержательный результат, чем фиксированная пауза перед одним чтением.
Для запросов, где redirect не является ожидаемым поведением, настройте его обработку явно. Иначе перенаправление на страницу входа может завершиться ответом 200 и ввести в заблуждение простой status-check. В отрицательных сценариях проверяйте также отсутствие нежелательного эффекта: отклонённая операция не должна создать запись, отправить событие или изменить остаток. Такие ожидания выбирают по контракту, чтобы тест не навязывал системе случайную внутреннюю реализацию.
Fixtures и жизненный цикл данных
Fixture описывает подготовку ресурса и гарантированную очистку после yield. Разделяйте пользователя, API-клиент и конкретный заказ, чтобы обязанности оставались понятными. Scope session подходит для неизменяемых общих ресурсов, но изменяемые сущности обычно требуют более узкой области жизни.
Очистка должна знать созданные идентификаторы и не удалять всё по общему признаку. Иначе параллельные тесты могут уничтожить данные друг друга. Если создание ресурса завершилось ошибкой до yield, продумайте, какие уже выполненные операции всё равно требуют освобождения.
Requests Session и конфигурация
Session помогает повторно использовать соединения и общие настройки. Однако её cookie и заголовки являются состоянием. Для разных ролей нужны отдельные экземпляры или явное управление, иначе запрос покупателя может случайно уйти с credentials администратора.
Общий клиент должен сокращать повторение: базовый URL, timeout и очистка чувствительных логов. Он не должен автоматически превращать любую ошибку в повтор запроса. Повтор изменяющей операции допустим только с понятной идемпотентностью и должен сам быть частью проверяемого поведения.
Практическая проверка доступа
Подготовьте двух пользователей и заказ первого. Первый читает запись и получает корректные данные. Второй обращается к тому же ID и получает предусмотренный контрактом отказ без тела чужого заказа. Затем проверьте анонимный запрос отдельно.
Ожидаемые статусы определяют требования API: 403 и 404 могут использоваться по разным причинам. QA проверяет согласованность политики и отсутствие утечки. Контроль только схемы JSON не обнаружит нарушение ownership, поэтому нужны конкретные значения и роли.
Ошибки и диагностика
Частые ошибки: проверять только status_code, отключать TLS-проверку для устранения любой ошибки, использовать реальные данные и хранить общую Session на весь параллельный набор. Ещё одна проблема — случайные данные без seed или сохранённых входов, из-за чего отказ невозможно повторить.
Сохраняйте метод, безопасный URL, статус, значимые поля и correlation id. Связка Pytest и Requests становится профессиональным инструментом тогда, когда каждый кейс имеет ясную причину существования, независимую подготовку и проверяемый результат операции.
Контрактные ожидания полезно согласовать с разработчиками до реализации: это предотвращает автоматизацию случайного текущего ответа как обязательного поведения продукта.