Анатомия тестового проекта на Python: раскладываем всё по полочкам для новичков

от автора

Устали хардкодить 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-автоматизации. Приведу очевидные на мой взгляд плюсы такого подхода:

  1. Чистота и читаемость тестов: Тесты описывают бизнес-сценарии (auth_api.login()), а не детали HTTP.

  2. Централизованное управление API: Если изменится эндпоинт, мы поправим его в одном месте — в соответствующем классе api/ (в этом моменте чуть не прослезился, почему я не знал это раньше..)

  3. Переиспользование кода: Фикстуры pytest и базовый API-клиент избавляют нас от тонн дублирования.

  4. Простота масштабирования: Появился новый сервис API? Просто создаем new_service_api.py и test_new_service.py — структура уже готова.

Конечно, это только начало. Дальше можно добавлять валидацию JSON-схем, генерацию тестовых данных с помощью Faker, data-driven тесты и многое другое. Но предложенная структура — это та самая крепкая база, с которой не стыдно начинать и которую легко развивать.

Надеюсь, это руководство поможет вам сделать первые шаги в API-автоматизации более осмысленными и продуктивными. Помните: инвестиции в хорошую структуру проекта окупаются сторицей!

Буду рад услышать ваши мысли, критику и советы в комментариях. Всем добра!


ссылка на оригинал статьи https://habr.com/ru/articles/929178/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *