Устали хардкодить 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 =
    testsAPI-клиенты
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 == 4013. Файл 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-автоматизации более осмысленными и продуктивными. Помните: инвестиции в хорошую структуру проекта окупаются сторицей!
Буду рад услышать ваши мысли, критику и советы в комментариях. Всем добра!
Комментарии (3)
 - aik-fiend19.07.2025 13:01- вы удивитесь, но в Python api-тесты можно писать вообще без тестовых библиотек, и requests не особо актуален когда есть httpx 
 - aik-fiend19.07.2025 13:01- то что в /api выглядит как api-gateways, соответственно: 
 1) лучше api-geteways вынести в /gateways
 2) /api переименовать в /endpoins и описать там конкретные эндпоинты, в которые можно спрятать такой код:- username = os.getenv("TEST_USERNAME")- password = os.getenv("TEST_PASSWORD")
 
           
 
rSedoy
На данный момент, в новых проекта pytest.ini и requirements.txt фактически не используются, а определяются в pyproject.toml, да и куча старых делают этот перенос