Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров4.4K

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

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

Теги:
Хабы:
+8
Комментарии3

Публикации

Ближайшие события