Устали хардкодить URL'ы и дублировать запросы? Разбираемся, как правильно организовать свой первый проект по автоматизации API на Pytest + Requests, чтобы он был красивым и расширяемым.
Привет, Хабровчане!
Меня зовут Кирилл, и я, как и многие здесь, иду по пути автоматизации тестирования. Сейчас будет немного лирики (совсем немного, чтобы как‑то подвести к сути:). Наверное, каждый «ручной» QA рано или поздно задумывается о том, что пора куда‑то расти. Такой момент настал и у меня и я выбрал автоматизацию тестирования. Самым приятным и реально доставляющим удовольствие от работы для меня стал ЯП Python. Помню свой первый успешный API‑тест, отправленный с помощью requests. Получил 200 OK в ответ, распарсил JSON и почувствовал себя настоящим хакером. Казалось, что теперь я могу проверить любую часть бэкенда, следующая остановка — Google :-)
Я начал писать тесты. Много тестов. Сначала всё шло хорошо, но со временем мой код начал превращаться в то, что называют на проектах «техническим долгом»:
Магия копипасты: Логика авторизации, одинаковые заголовки, базовые URL'ы — всё это кочевало из одного тестового файла в другой.
Смешение всего со всем: Логика отправки HTTP-запроса была тесно переплетена с логикой самой проверки (ассертами). Тесты становились трудночитаемыми.
Хрупкость: Стоило разработчикам поменять базовый URL с v1 на v2, и мне приходилось лезть в десятки файлов, чтобы всё исправить. Неприятненько и нудно.
Поскольку у меня отсутствует тяга к неприятному, я начал смотреть, как устроены проекты у более опытных коллег, читать статьи и собирать лучшие практики. Сегодня я хочу поделиться простой, но эффективной структурой, которая поможет любому новичку сразу начать строить чистый, поддерживаемый и легко расширяемый проект для автоматизации API. Сразу оговорюсь, что данная статья является результатом личного опыта и, возможно, более опытные коллеги-автоматизаторы найдут тут какие-то изъяны. В таком случае, я буду только рад конструктивной критике и замечаниям.
Шаг 0: Наш инструментарий
Не будем усложнять. Для старта нам понадобится проверенный временем набор:
pytest: Мощный и гибкий фреймворк для тестирования на Python. Его фикстуры и простой синтаксис — это просто подарок.
requests: Стандартная библиотека для выполнения HTTP-запросов. Простая, но очень мощная.
python-dotenv: Для хранения переменных окружения (базовые URL'ы, логины, токены) отдельно от кода.
Создадим файл requirements.txt в корне нашего будущего проекта:
pytest
requests
python-dotenv
После чего, установим все, что перечислили в файле, одной командой:
pip install -r requirements.txt
Шаг 1: Скелет API-проекта
Хороший проект начинается с хорошей организации. Вот базовый и, достаточно простой на мой взгляд скелет, который мы будем использовать:
my_api_tests/
├── api/
│ ├── __init__.py
│ ├── base_api_client.py # Базовый класс для работы с API
│ ├── auth_api.py # Класс для работы с эндпоинтами аутентификации
│ └── users_api.py # Класс для работы с эндпоинтами пользователей
│
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Фикстуры для тестов (клиенты, данные)
│ └── test_auth.py # Тесты для API аутентификации
│ └── test_users.py # Тесты для API пользователей
│
├── .env # Всяческие переменные окружения (логины, пароли и т.д. лучше держать здесь)
├── .gitignore # Сюда записываем все, что не должно попасть в GIT (.env обязательно сюда)
├── pytest.ini # Конфигурация pytest
└── requirements.txt # Уже знакомый нам файл с инструментарием
Тут постараюсь кратко объяснить что есть что в этой структуре:
директория api/ — это "сердце" нашего фреймворка. Здесь мы будем описывать, как взаимодействовать с различными частями нашего API. Каждая "сущность" (пользователи, продукты, заказы) получит свой класс.
base_api_client.py — это наш фундамент для всех API-клиентов. Он будет содержать общую логику отправки запросов (GET, POST и т.д.), обработку базового URL и заголовков.
auth_api.py, users_api.py — это конкретные классы-клиенты. Они наследуются от base_api_client и содержат методы для работы с конкретными эндпоинтами (например, /login или /users).
директория tests/ — папка, где живут наши тесты. pytest будет автоматически находить их здесь.
conftest.py — Всё, что мы в нём определим (фикстуры), будет доступно во всех тестах. Идеальное место, чтобы создавать API-клиентов или подготавливать тестовые данные.
.env — файл для хранения секретных данных (URL, логины, пароли). Важно: никогда не добавляйте его в Git! Это ОЧЕНЬ важно, поэтому в самой структуре я также оставил коммент с упоминанием о том, что этот файл всегда должен быть добавлен в .gitignore (если мы, конечно, не хотим, чтобы наши логопассы утекли к посторонним лицам).
.gitignore — стандартный файл для Git, который поможет игнорировать ненужные файлы (.env, pycache/ и т.д.) и не грузить их в удаленный репозиторий.
pytest.ini — конфигурационный файл для pytest. Здесь можно задать маркеры, пути к тестам и другие настройки.
Шаг 2: Наполняем скелет жизнью
Теперь давайте напишем немного кода, чтобы всё это заработало. Представим, что у нас есть простое API с эндпоинтами для аутентификации и получения списка пользователей.
Конфигурация
1. Файл .env (в корне проекта)
BASE_URL="http://localhost:5000/api/v1" # Пример базового URL
TEST_USERNAME="user"
TEST_PASSWORD="password123"
2. Файл pytest.ini (в корне проекта)
[pytest]
markers =
auth: authentication testing
users: users management testing
testpaths =
tests
API-клиенты
1. Файл api/base_api_client.py
Этот класс будет содержать requests.Session() для поддержания сессии (что полезно для cookies и заголовков) и общие методы для всех HTTP-запросов.
import requests
import json
class BaseApiClient:
def __init__(self, base_url: str):
self.base_url = base_url
self.session = requests.Session()
def _send_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
url = f"{self.base_url}{endpoint}"
try:
response = self.session.request(method, url, **kwargs)
response.raise_for_status() # Выбросит исключение для 4xx/5xx ответов
return response
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
raise
def get(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request("GET", endpoint, **kwargs)
def post(self, endpoint: str, **kwargs) -> requests.Response:
return self._send_request("POST", endpoint, **kwargs)
Методы _send_request и raise_for_status() — это мощный инструмент для централизованной обработки ошибок. Подсмотрел это у своего более опытного коллеги (Спасибо, Женя!:)
2. Файл api/auth_api.py
Это класс-клиент, который знает всё об эндпоинтах аутентификации.
from api.base_api_client import BaseApiClient
class AuthApi(BaseApiClient):
AUTH_LOGIN_ENDPOINT = "/auth/login"
def login(self, username, password) -> dict:
"""Выполняет вход пользователя и возвращает JSON ответа."""
payload = {"username": username, "password": password}
response = self.post(self.AUTH_LOGIN_ENDPOINT, json=payload)
return response.json()
3. Файл api/users_api.py
А этот класс отвечает за работу с пользователями.
from api.base_api_client import BaseApiClient
class UsersApi(BaseApiClient):
USERS_ENDPOINT = "/users"
def get_users(self, token: str) -> dict:
"""Получает список пользователей, требуя токен авторизации."""
headers = {"Authorization": f"Bearer {token}"}
response = self.get(self.USERS_ENDPOINT, headers=headers)
return response.json()
Тесты и Фикстуры
1. Файл tests/conftest.py
Здесь мы создадим фикстуры, которые будут "собирать" наши API-клиенты и предоставлять их тестам.
import pytest
import os
from dotenv import load_dotenv
from api.auth_api import AuthApi
from api.users_api import UsersApi
load_dotenv()
@pytest.fixture(scope="session")
def base_url():
"""Фикстура, возвращающая базовый URL из .env."""
return os.getenv("BASE_URL")
@pytest.fixture(scope="function")
def auth_api(base_url):
"""Фикстура для создания клиента AuthApi."""
return AuthApi(base_url)
@pytest.fixture(scope="function")
def users_api(base_url):
"""Фикстура для создания клиента UsersApi."""
return UsersApi(base_url)
@pytest.fixture(scope="function")
def auth_token(auth_api) -> str:
"""Фикстура, которая логинится и возвращает токен авторизации."""
username = os.getenv("TEST_USERNAME")
password = os.getenv("TEST_PASSWORD")
response_data = auth_api.login(username, password)
return response_data.get("token")
Фикстура auth_token — это наш ключ к чистому коду. Она сама логинится и отдает токен. Тестам, требующим авторизации, больше не нужно об этом беспокоиться.
2. Файл tests/test_auth.py
А вот и сами тесты. Обратите внимание, какими они стали лаконичными! Мне кажется, даже ваш project manager, который не сильно шарит в коде, разберется что тут к чему)
import pytest
import os
import requests
@pytest.mark.auth
def test_successful_login(auth_api):
"""Проверяет успешный вход в систему."""
username = os.getenv("TEST_USERNAME")
password = os.getenv("TEST_PASSWORD")
response_data = auth_api.login(username, password)
assert response_data["status"] == "success"
assert "token" in response_data
@pytest.mark.auth
def test_login_with_invalid_credentials(auth_api):
"""Проверяет, что система вернет ошибку на неверные данные."""
with pytest.raises(requests.exceptions.HTTPError) as excinfo:
auth_api.login("wrong_user", "wrong_password")
assert excinfo.value.response.status_code == 401
3. Файл tests/test_users.py
Тест, использующий фикстуру с токеном.
import pytest
@pytest.mark.users
def test_get_users_list_is_successful(users_api, auth_token):
"""Проверяет получение списка пользователей после авторизации."""
users_list_data = users_api.get_users(token=auth_token)
assert users_list_data["status"] == "success"
assert isinstance(users_list_data.get("users"), list)
Вот, в принципе и все! Что мы имеем в итоге?
А в итоге мы с вами построили крепкий, но простой фундамент для API-автоматизации. Приведу очевидные на мой взгляд плюсы такого подхода:
Чистота и читаемость тестов: Тесты описывают бизнес-сценарии (auth_api.login()), а не детали HTTP.
Централизованное управление API: Если изменится эндпоинт, мы поправим его в одном месте — в соответствующем классе api/ (в этом моменте чуть не прослезился, почему я не знал это раньше..)
Переиспользование кода: Фикстуры pytest и базовый API-клиент избавляют нас от тонн дублирования.
Простота масштабирования: Появился новый сервис API? Просто создаем new_service_api.py и test_new_service.py — структура уже готова.
Конечно, это только начало. Дальше можно добавлять валидацию JSON-схем, генерацию тестовых данных с помощью Faker, data-driven тесты и многое другое. Но предложенная структура — это та самая крепкая база, с которой не стыдно начинать и которую легко развивать.
Надеюсь, это руководство поможет вам сделать первые шаги в API-автоматизации более осмысленными и продуктивными. Помните: инвестиции в хорошую структуру проекта окупаются сторицей!
Буду рад услышать ваши мысли, критику и советы в комментариях. Всем добра!