API автотесты на Python с запуском на CI/CD и Allure отчетом

API автотесты на Python с запуском на CI/CD и Allure отчетом

Вступление

В этой статье мы разберём процесс написания API автотестов на Python, используя современные best practices. Кроме того, мы настроим их запуск в CI/CD с помощью GitHub Actions и сформируем Allure-отчёт с историей запусков. Цель статьи — не только показать, как писать качественные API автотесты, но и научить запускать их в CI/CD, получая удобные отчёты о результатах.

Мы будем использовать GitHub Actions, но аналогичная конфигурация возможна и для других CI/CD-систем, таких как GitLab CI, CircleCI или Jenkins — отличаться будет только синтаксис. Итоговый Allure-отчёт опубликуем на GitHub Pages, а также настроим сохранение истории запусков.

Тестируемый API — REST API, предоставленный сервисом FakeBank. Это учебное API, позволяющее работать с фейковыми банковскими операциями. Для тестирования будем отправлять запросы к https://api.sampleapis.com/fakebank/accounts.

Технологии

Вот стек инструментов, которые мы будем использовать:

  • Python 3.12 — для написания автотестов
  • Pytest — тестовый фреймворк
  • HTTPX — для отправки запросов к REST API
  • Pydantic — для сериализации, десериализации и валидации данных
  • Pydantic Settings — для удобной работы с конфигурацией проекта
  • Faker — для генерации случайных данных
  • Allure — для генерации детализированного отчёта
  • jsonschema — для валидации схемы JSON-ответов
  • pytest-xdist — для параллельного запуска тестов

Почему HTTPX, а не Requests?

Библиотека Requests хоть и популярна, но уже давно перестала активно развиваться. У неё до сих пор нет встроенной аннотации типов, хотя эта возможность появилась в Python ещё в версии 3.5 (а на момент написания статьи скоро выйдет уже 3.14). Прошло более 10 лет, но в Requests так и не добавили полноценную поддержку аннотаций типов, что говорит о слабой эволюции библиотеки.

Requests в своё время завоевала популярность благодаря простоте и отсутствию достойных альтернатив. Но сегодня есть более современное, мощное и удобное решение — HTTPX.

Что даёт HTTPX по сравнению с Requests:

  • Встроенные аннотации типов
  • Удобный объект Client для повторного использования соединений
  • Event hooks (хуки для обработки событий)
  • Полноценная поддержка асинхронных запросов
  • Поддержка HTTP/2
  • Современная и понятная документация
  • Множество других полезных возможностей

Всё это делает HTTPX более гибким, производительным и удобным инструментом. Requests, скорее всего, так и останется на уровне синхронных запросов без поддержки современных возможностей.

Если вы не пишете legacy-код и создаёте новый проект API автотестов на Python, то HTTPX — очевидный выбор.

Почему Pydantic?

Тут всё просто: Pydantic — это стандарт де-факто для работы с данными в Python.

Он позволяет:

  • Валидировать данные на основе аннотаций типов
  • Удобно сериализовать/десериализовать JSON
  • Гарантировать строгую типизацию на уровне данных
  • Работать с конфигурацией проекта через Pydantic Settings

У Pydantic почти нет достойных альтернатив. Стандартные dataclasses в Python даже близко не дают такой же функциональности, особенно валидации и строгой типизации данных.

Если вам нужны чистые, предсказуемые и надёжные данные — Pydantic обязателен в любом современном проекте.

Почему pytest?

Потому что pytest — это лучший тестовый фреймворк для Python.

  • Гибкость: можно писать как простые, так и сложные тесты
  • Мощность: плагины, фикстуры, параметризация, маркировки, перезапуски, интеграция с Allure
  • Читаемость: тесты остаются чистыми и понятными
  • Популярность: pytest — стандарт для тестирования в Python

На фоне этого Behave, Robot Framework выглядят избыточными и усложняющими жизнь инструментами.

  • Лишняя абстракция — увеличивает сложность написания и поддержки тестов
  • Сложность отладки — тесты в стиле Gherkin выглядят красиво, но на практике мешают глубоко анализировать ошибки
  • Мнимая выгода читабельности — грамотно написанные тесты на pytest будут понятнее, чем сценарии Gherkin

Поэтому если вам нужны мощные, поддерживаемые и удобные API/UI-тесты — pytest это лучший выбор.

Модели для описания структур данных

Прежде чем работать с API https://api.sampleapis.com/fakebank/accounts, необходимо определить структуру данных, которые мы будем отправлять и получать.

Для этого используем Pydantic, так как он:

  • Позволяет автоматически валидировать данные
  • Поддерживает сериализацию и десериализацию JSON
  • Позволяет использовать встроенные валидаторы, такие как HttpUrl и EmailStr, для проверки корректности данных
  • Обеспечивает удобную типизацию

Мы определим:

  1. CreateOperationSchema – модель для создания новой операции
  2. UpdateOperationSchema – модель для обновления данных (используется для PATCH запросов)
  3. OperationSchema – расширенная модель с id, представляющая конечную структуру операции
  4. OperationsSchema – контейнер для списка операций
{ "id": 25, "debit": 6.99, "credit": null, "category": "Merchandise", "description": "Benderbräu", "transactionDate": "2016-02-25" }
from datetime import date from pydantic import BaseModel, Field, RootModel, ConfigDict class CreateOperationSchema(BaseModel): """ Модель для создания новой банковской операции. Поля: - debit (float | None): Сумма списания со счёта - credit (float | None): Сумма зачисления на счёт - category (str): Категория операции - description (str): Описание операции - transaction_date (date): Дата транзакции (передаётся в формате "transactionDate") """ model_config = ConfigDict(populate_by_name=True) # Позволяет использовать alias при сериализации/десериализации debit: float | None credit: float | None category: str description: str transaction_date: date = Field(alias="transactionDate") # Указываем alias для соответствия API class UpdateOperationSchema(BaseModel): """ Модель для обновления банковской операции (используется в PATCH запросах). Все поля являются необязательными, так как можно обновлять только часть данных. Поля: - debit (float | None): Новая сумма списания - credit (float | None): Новая сумма зачисления - category (str | None): Новая категория - description (str | None): Новое описание - transaction_date (date | None): Новая дата транзакции (alias "transactionDate") """ model_config = ConfigDict(populate_by_name=True) debit: float | None credit: float | None category: str | None description: str | None transaction_date: date | None = Field(alias="transactionDate") class OperationSchema(CreateOperationSchema): """ Модель банковской операции, содержащая ID. Наследуется от CreateOperationSchema и добавляет поле: - id (int): Уникальный идентификатор операции """ id: int class OperationsSchema(RootModel): """ Контейнер для списка операций. Поле: - root (list[OperationSchema]): Список операций """ root: list[OperationSchema]
  • CreateOperationSchema – используется при создании новой операции. Включает поля для суммы списания (debit), суммы зачисления (credit), категории (category), описания (description) и даты (transaction_date).
  • UpdateOperationSchema – предназначена для PATCH-запросов, где можно передавать только изменяемые поля. Все параметры здесь опциональны, так как частичное обновление не требует передачи всех данных.
  • OperationSchema – расширенная версия CreateOperationSchema, добавляет поле id, которое присваивается сервером. Используется для представления операции в ответах API.
  • OperationsSchema – список операций. Наследуется от RootModel, что позволяет работать с массивами объектов в Pydantic.

Почему Pydantic, а не TypedDict или dataclasses?

Pydantic – это лучшее решение для работы с API-данными.

  • TypedDict и NamedTuple – подходят больше для аннотаций типов, но не для валидации данных и сериализации.
  • dataclasses – могут быть альтернативой, но у них нет встроенной валидации и сериализации JSON. Они работают хорошо в локальных моделях, но для API Pydantic – гораздо удобнее.

Pydantic позволяет автоматически проверять данные, использовать алиасы для полей и работать с JSON без дополнительных преобразований.

Генерация фейковых данных

При тестировании API нам нужно создавать множество случайных данных для разных сценариев. Чтобы автоматизировать этот процесс и избавиться от ручного заполнения, используем библиотеку Faker и реализуем класс Fake. Класс Fake будет предоставлять удобный интерфейс для генерации нужных значений.

from datetime import date, timedelta from faker import Faker class Fake: """ Класс-обертка над Faker, предоставляющий удобные методы генерации фейковых данных для банковских операций. """ def __init__(self, faker: Faker): """ Инициализирует объект Fake с экземпляром Faker. :param faker: Экземпляр Faker для генерации случайных данных. """ self.faker = faker def date(self, start: timedelta = timedelta(days=-30), end: timedelta = timedelta()) -> date: """ Генерирует случайную дату в заданном диапазоне. :param start: Начальный диапазон (по умолчанию -30 дней от текущей даты). :param end: Конечный диапазон (по умолчанию сегодняшняя дата). :return: Случайная дата в заданном диапазоне. """ return self.faker.date_between(start_date=start, end_date=end) def money(self, start: float = -100, end: float = 100) -> float: """ Генерирует случайную сумму денег. :param start: Минимальное значение (по умолчанию -100). :param end: Максимальное значение (по умолчанию 100). :return: Случайное число с плавающей запятой в заданном диапазоне. """ return self.faker.pyfloat(min_value=start, max_value=end) def category(self) -> str: """ Генерирует случайную категорию расходов. :return: Одна из предопределенных категорий ('food', 'taxi', 'fuel' и т.д.). """ return self.faker.random_element(['food', 'taxi', 'fuel', 'beauty', 'restaurants']) def sentence(self) -> str: """ Генерирует случайное описание операции. :return: Строка с описанием. """ return self.faker.sentence() # Создаем глобальный экземпляр `fake`, который будем использовать в других модулях. fake = Fake(faker=Faker())

Класс Fake инкапсулирует логику библиотеки Faker и предоставляет удобный API для работы. Теперь вместо множества вызовов Faker().some_method() в коде можно просто использовать fake.some_method().

Теперь добавим фейковую генерацию прямо в модели Pydantic, используя параметр default_factory. Это позволит автоматически заполнять поля случайными значениями при создании модели.

from datetime import date from pydantic import BaseModel, Field, RootModel, ConfigDict from tools.fakers import fake class CreateOperationSchema(BaseModel): """ Модель для создания новой банковской операции. Поля: - debit (float | None): Сумма списания со счёта - credit (float | None): Сумма зачисления на счёт - category (str): Категория операции - description (str): Описание операции - transaction_date (date): Дата транзакции (передаётся в формате "transactionDate") """ model_config = ConfigDict(populate_by_name=True) debit: float | None = Field(default_factory=fake.money) # Генерация случайной суммы списания со счёта credit: float | None = Field(default_factory=fake.money) # Генерация случайной суммы зачисления на счёт category: str = Field(default_factory=fake.category) # Генерация случайной категории description: str = Field(default_factory=fake.sentence) # Генерация случайного описания transaction_date: date = Field(alias="transactionDate", default_factory=fake.date) # Генерация случайной даты class UpdateOperationSchema(BaseModel): """ Модель для обновления банковской операции (используется в PATCH запросах). Все поля являются необязательными, так как можно обновлять только часть данных. Поля: - debit (float | None): Новая сумма списания - credit (float | None): Новая сумма зачисления - category (str | None): Новая категория - description (str | None): Новое описание - transaction_date (date | None): Новая дата транзакции (alias "transactionDate") """ model_config = ConfigDict(populate_by_name=True) debit: float | None = Field(default_factory=fake.money) credit: float | None = Field(default_factory=fake.money) category: str | None = Field(default_factory=fake.category) description: str | None = Field(default_factory=fake.sentence) transaction_date: date | None = Field(alias="transactionDate", default_factory=fake.date) class OperationSchema(CreateOperationSchema): """ Модель банковской операции, содержащая ID. Наследуется от CreateOperationSchema и добавляет поле: - id (int): Уникальный идентификатор операции """ id: int class OperationsSchema(RootModel): """ Контейнер для списка операций. Поле: - root (list[OperationSchema]): Список операций """ root: list[OperationSchema]

Применение default_factory. Каждое поле получает случайное значение при создании экземпляра модели. Пример работы:

operation=CreateOperationSchema()print(operation)

Вывод (данные случайные):

{ "debit": -25.4, "credit": 87.6, "category": "fuel", "description": "Paid for fuel at a gas station.", "transactionDate": "2025-03-30"}

Настройки API автотестов

Реализуем централизованный подход к управлению настройками для API автотестов. Это позволит легко изменять параметры без необходимости редактировать код.

В данном примере нам нужно хранить только URL API и таймаут запросов, но в будущем можно расширять этот механизм.

Для управления настройками будем использовать Pydantic Settings — удобную библиотеку, которая позволяет загружать переменные окружения в виде Pydantic-моделей.

from pydantic import BaseModel, HttpUrl from pydantic_settings import BaseSettings, SettingsConfigDict class HTTPClientConfig(BaseModel): """ Настройки HTTP-клиента. Поля: url (HttpUrl): Базовый URL для API. timeout (float): Таймаут для запросов в секундах. """ url: HttpUrl timeout: float @property def client_url(self) -> str: """ Возвращает URL API в строковом формате. """ return str(self.url) class Settings(BaseSettings): """ Главная модель для хранения всех настроек проекта. Загружает переменные из файла `.env` и поддерживает вложенные структуры. Поля: fake_bank_http_client (HTTPClientConfig): Настройки HTTP-клиента. """ model_config = SettingsConfigDict( env_file='.env', # Загружаем переменные из файла .env env_file_encoding='utf-8', # Указываем кодировку файла env_nested_delimiter='.' # Позволяет использовать вложенные переменные (FAKE_BANK_HTTP_CLIENT.URL) ) fake_bank_http_client: HTTPClientConfig # Настройки HTTP-клиента
  • Класс HTTPClientConfigНаследуется от BaseModel (Pydantic).Описывает базовые настройки HTTP-клиента:url: HttpUrl — базовый адрес API (Pydantic автоматически проверит, что это корректный URL).timeout: float — таймаут запросов.Добавили @property client_url, чтобы возвращать url в строковом формате.
  • Класс SettingsНаследуется от BaseSettings (Pydantic Settings).model_config определяет:Где искать переменные (из файла .env).Кодировку файла (utf-8).Поддержку вложенных переменных (env_nested_delimiter='.').fake_bank_http_client: HTTPClientConfig — добавляет вложенные настройки для HTTP-клиента.
FAKE_BANK_HTTP_CLIENT.URL="https://api.sampleapis.com" FAKE_BANK_HTTP_CLIENT.TIMEOUT=100
  • FAKE_BANK_HTTP_CLIENT.URL — адрес API, который будет использовать HTTP-клиент.
  • FAKE_BANK_HTTP_CLIENT.TIMEOUT — таймаут запросов (в секундах).
  • Благодаря env_nested_delimiter='.', переменные в файле .env автоматически конвертируются в вложенные структуры внутри Settings.

Теперь можно просто инициализировать Settings и использовать его:

settings = Settings() print(settings.fake_bank_http_client.client_url) # "https://api.sampleapis.com" print(settings.fake_bank_http_client.timeout) # 100

Важно! Глобальную переменную settings добавлять не будем — вместо этого будем инициализировать settings на уровне фикстур.

API клиенты

Теперь реализуем API клиент для работы с API https://api.sampleapis.com/fakebank/accounts. Однако перед этим создадим базовый API клиент, который будет использоваться для выполнения стандартных HTTP-запросов. В качестве HTTP-клиента будем использовать httpx.Client.

from typing import Any import allure from httpx import Client, URL, Response, QueryParams from httpx._types import RequestData, RequestFiles from config import HTTPClientConfig class BaseClient: """ Базовый клиент для выполнения HTTP-запросов. Этот класс предоставляет основные методы для выполнения HTTP-запросов (GET, POST, PATCH, DELETE) и использует httpx.Client для выполнения запросов. Каждый метод добавлен с использованием allure для генерации отчетов о тестах. """ def __init__(self, client: Client): """ Инициализация клиента. :param client: Экземпляр httpx.Client """ self.client = client @allure.step("Make GET request to {url}") def get(self, url: URL | str, params: QueryParams | None = None) -> Response: """ Выполняет GET-запрос. :param url: URL эндпоинта :param params: Query параметры запроса :return: HTTP-ответ """ return self.client.get(url, params=params) @allure.step("Make POST request to {url}") def post( self, url: URL | str, json: Any | None = None, data: RequestData | None = None, files: RequestFiles | None = None ) -> Response: """ Выполняет POST-запрос. :param url: URL эндпоинта :param json: JSON тело запроса :param data: Данные формы :param files: Файлы для загрузки :return: HTTP-ответ """ return self.client.post(url, json=json, data=data, files=files) @allure.step("Make PATCH request to {url}") def patch(self, url: URL | str, json: Any | None = None) -> Response: """ Выполняет PATCH-запрос. :param url: URL эндпоинта :param json: JSON тело запроса :return: HTTP-ответ """ return self.client.patch(url, json=json) @allure.step("Make DELETE request to {url}") def delete(self, url: URL | str) -> Response: """ Выполняет DELETE-запрос. :param url: URL эндпоинта :return: HTTP-ответ """ return self.client.delete(url) def get_http_client(config: HTTPClientConfig) -> Client: """ Функция для инициализации HTTP-клиента. :param config: Конфигурация HTTP-клиента :return: Экземпляр httpx.Client """ return Client( timeout=config.timeout, # Устанавливаем таймаут для всех запросов base_url=config.client_url, # Базовый URL для API )
  • BaseClient — класс, который инкапсулирует базовые HTTP-методы (GET, POST, PATCH, DELETE) для взаимодействия с API. Для каждого метода добавлен декоратор allure.step, который позволяет отслеживать шаги выполнения тестов в отчете.
  • get_http_client — функция для создания экземпляра httpx.Client с необходимыми настройками (например, URL и таймаут), переданными через конфигурацию.

Важно! Чтобы избежать ошибок и дублирования адресов эндпоинтов в проекте, рекомендуется вынести все URI в отдельный Enum. Это позволит централизованно управлять URL-адресами и избежать опечаток.

from enum import Enum class APIRoutes(str, Enum): """ Перечисление всех URI-адресов API для проекта. Это перечисление позволяет централизованно управлять всеми маршрутами API, что помогает избежать ошибок при их использовании и упрощает масштабирование. """ CARDS = "/fakebank/cards" CLIENTS = "/fakebank/clients" OPERATIONS = "/fakebank/accounts" # Основной URI для работы с операциями STATEMENTS = "/fakebank/statements" NOTIFICATIONS = "/fakebank/notifications" def __str__(self): return self.value
  • APIRoutes — перечисление всех возможных эндпоинтов, с которыми будет работать приложение. Это позволяет централизовать и стандартизировать использование адресов.
  • В реальных проектах вам возможно придется добавлять новые маршруты, и это будет намного удобнее, если они будут прописаны в одном месте.

Теперь напишем API клиент для работы с операциями, используя API https://api.sampleapis.com/fakebank/accounts. Опишем методы, которые необходимо реализовать для работы с операциями в API:

  • get_operations_api — получит список операций.
  • get_operation_api — получит информацию об операции по operation_id.
  • create_operation_api — создаст операцию и вернет её данные.
  • update_operation_api — обновит операцию по operation_id и вернет обновленные данные.
  • delete_operation_api — удалит операцию по operation_id
import allure from httpx import Response from clients.base_client import BaseClient, get_http_client from config import Settings from schema.operations import CreateOperationSchema, UpdateOperationSchema, OperationSchema from tools.routes import APIRoutes class OperationsClient(BaseClient): """ Клиент для взаимодействия с операциями. """ @allure.step("Get list of operations") def get_operations_api(self) -> Response: """ Получить список всех операций. :return: Ответ от сервера с информацией о всех операциях. """ return self.get(APIRoutes.OPERATIONS) @allure.step("Get operation by id {operation_id}") def get_operation_api(self, operation_id: int) -> Response: """ Получить операцию по идентификатору. :param operation_id: Идентификатор операции. :return: Ответ от сервера с информацией об операции. """ return self.get(f"{APIRoutes.OPERATIONS}/{operation_id}") @allure.step("Create operation") def create_operation_api(self, operation: CreateOperationSchema) -> Response: """ Создать операцию. :param operation: Данные для создания новой операции. :return: Ответ от сервера с информацией о созданной операции. """ return self.post( APIRoutes.OPERATIONS, json=operation.model_dump(mode='json', by_alias=True) # Сериализуем объект в JSON перед отправкой ) @allure.step("Update operation by id {operation_id}") def update_operation_api( self, operation_id: int, operation: UpdateOperationSchema ) -> Response: """ Обновить операцию по идентификатору. :param operation_id: Идентификатор операции, которую нужно обновить. :param operation: Данные для обновления операции. :return: Ответ от сервера с обновленными данными операции. """ return self.patch( f"{APIRoutes.OPERATIONS}/{operation_id}", json=operation.model_dump(mode='json', by_alias=True, exclude_none=True) ) @allure.step("Delete operation by id {operation_id}") def delete_operation_api(self, operation_id: int) -> Response: """ Удалить операцию по идентификатору. :param operation_id: Идентификатор операции, которую нужно удалить. :return: Ответ от сервера с результатом удаления операции. """ return self.delete(f"{APIRoutes.OPERATIONS}/{operation_id}") def create_operation(self) -> OperationSchema: """ Упрощенный метод для создания новой операции. Этот метод создает операцию с помощью схемы `CreateOperationSchema`, отправляет запрос на создание, а затем преобразует ответ в объект `OperationSchema`. :return: Объект `OperationSchema`, представляющий созданную операцию. """ # Создаем запрос с фейковыми данными (по умолчанию для теста) request = CreateOperationSchema() # Отправляем запрос на создание response = self.create_operation_api(request) # Возвращаем созданную операцию как объект схемы return OperationSchema.model_validate_json(response.text) def get_operations_client(settings: Settings) -> OperationsClient: """ Функция для создания экземпляра OperationsClient с нужными настройками. :param settings: Конфигурация с настройками для работы с API. :return: Экземпляр клиента для работы с операциями. """ return OperationsClient(client=get_http_client(settings.fake_bank_http_client))
  • OperationsClient — класс, который наследует от BaseClient и предоставляет методы для работы с операциями (получение списка операций, создание, обновление, удаление операции).
  • Каждый метод аннотирован с помощью @allure.step, что позволяет добавлять шаги в отчет Allure. Это помогает отслеживать выполнение шагов и параметров запросов в тестах.
  • Для сериализации объектов в JSON используется метод model_dump, который поддерживает алиасы и может исключать поля с None значениями.
  • get_operations_client — функция для создания экземпляра OperationsClient. Она принимает настройки и использует их для создания HTTP-клиента с нужными параметрами.
  • get_http_client — этот метод создает экземпляр httpx.Client с настройками из конфигурации.

Важно! Обратите внимание, что шаги для Allure были добавлены на двух уровнях: для BaseClient и OperationsClient.

  1. Шаги для BaseClient: На уровне BaseClient шаги содержат подробную техническую информацию о том, какой HTTP-метод использовался (например, GET, POST, PATCH, DELETE), куда был отправлен запрос, с каким телом и параметрами. Это позволяет нам точно видеть все детали запроса, которые были отправлены на сервер. Например, мы можем узнать:Используемый HTTP-метод.URL, по которому был отправлен запрос.Тело запроса (если оно было).
  2. Шаги для OperationsClient: На уровне OperationsClient шаги отражают описание выполняемых действий с точки зрения бизнес-логики. Здесь не отображаются технические детали (например, сам HTTP-запрос), а скорее бизнесовые действия, такие как "Получение списка операций", "Создание новой операции" и т.д. Это делает отчет Allureболее понятным и ориентированным на бизнес-логику, не перегружая его техническими деталями.В случае, если нужно получить более детальную информацию (например, какие параметры были переданы в запросе или какие методы использовались), можно раскрыть шаги с детальным описанием, где будут представлены все технические детали.
  3. Декоратор @allure.step: Важно отметить, что мы используем декоратор @allure.step специально, чтобы в отчет автоматически прикреплялись все параметры, передаваемые в методы и функции. Это позволяет нам в отчете видеть не только, какие шаги были выполнены, но и какие данные были переданы в каждом запросе, обеспечивая полную прозрачность всех действий.

Таким образом, с помощью такого подхода мы достигаем:

  • Гибкости в отчете, где можно раскрывать нужные детали on-demand.
  • Чистоты и понятности отчета, где бизнес-логику и технические детали можно разделить.
API автотесты на Python с запуском на CI/CD и Allure отчетом

Логирование взаимодействий с API

Для удобного анализа логов при запуске на CI/CD, а также при локальной отладке, добавим логирование HTTP-запросов и ответов. Это позволит:

  • Видеть, какие запросы отправляются (метод, URL).
  • Получать информацию о статусе ответа и причине, если запрос завершился неудачно.
  • Анализировать взаимодействие с API без необходимости включать дебаг-режим или просматривать трассировки сети.

При этом важно избежать избыточного логирования, чтобы не засорять логи ненужной информацией. Поэтому мы ограничимся следующими записями:

  • Логирование запроса: указываем HTTP-метод и URL.
  • Логирование ответа: указываем статус-код, текстовое описание (reason phrase) и URL.

Некоторые QA Automation также добавляют cURL-команды в логи для воспроизведения запросов, но это может излишне увеличивать объем логов. Вместо этого мы можем прикреплять cURL-команду в Allure-отчет, где она будет более полезной в контексте тестов.

Первым шагом создадим функцию-билдер для конфигурации логгера с пользовательскими настройками.

import logging def get_logger(name: str) -> logging.Logger: """ Создает и настраивает логгер с заданным именем. :param name: Имя логгера. :return: Объект логгера. """ logger = logging.getLogger(name) logger.setLevel(logging.DEBUG) # Устанавливаем уровень логирования handler = logging.StreamHandler() # Создаем обработчик вывода в консоль handler.setLevel(logging.DEBUG) # Устанавливаем уровень для обработчика # Формат логов: время | имя логгера | уровень логирования | сообщение formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) # Добавляем обработчик к логгеру return logger
  • get_logger(name: str) -> logging.Logger — создает логгер с кастомными настройками.
  • Уровень DEBUG — используется, чтобы видеть все события, включая информационные и отладочные.
  • Обработчик StreamHandler() — выводит логи в консоль.
  • Формат сообщений включает:Время событияИмя логгераУровень логированияСообщение
  • Логгер можно переиспользовать в любом файле, вызывая get_logger("Имя").

Библиотека HTTPX предоставляет механизм event hooks, который позволяет выполнять кастомные действия до отправки запроса и после получения ответа. Мы используем этот механизм для логирования.

from httpx import Request, Response from tools.logger import get_logger # Создаем логгер для HTTP-клиента logger = get_logger("HTTP_CLIENT") def log_request_event_hook(request: Request): """ Логирует информацию перед отправкой HTTP-запроса. :param request: Объект запроса HTTPX. """ logger.info(f'Make {request.method} request to {request.url}') def log_response_event_hook(response: Response): """ Логирует информацию после получения HTTP-ответа. :param response: Объект ответа HTTPX. """ logger.info( f"Got response {response.status_code} {response.reason_phrase} from {response.url}" )
  • log_request_event_hook(request: Request)Логирует HTTP-метод (GET, POST и т. д.) и URL перед отправкой запроса.
  • log_response_event_hook(response: Response)Логирует HTTP-статус-код, причину ответа (reason_phrase) и URL после получения ответа.
  • Оба хука подключаются к клиенту HTTPX и автоматически выполняются при каждом запросе.

Теперь добавим хук-функции к HTTP-клиенту, чтобы логи автоматически писались при каждом запросе.

from typing import Any import allure from httpx import Client, URL, Response, QueryParams from httpx._types import RequestData, RequestFiles from config import HTTPClientConfig class BaseClient: """ Базовый клиент для выполнения HTTP-запросов. Этот класс предоставляет основные методы для выполнения HTTP-запросов (GET, POST, PATCH, DELETE) и использует httpx.Client для выполнения запросов. Каждый метод добавлен с использованием allure для генерации отчетов о тестах. """ def __init__(self, client: Client): """ Инициализация клиента. :param client: Экземпляр httpx.Client """ self.client = client @allure.step("Make GET request to {url}") def get(self, url: URL | str, params: QueryParams | None = None) -> Response: """ Выполняет GET-запрос. :param url: URL эндпоинта :param params: Query параметры запроса :return: HTTP-ответ """ return self.client.get(url, params=params) @allure.step("Make POST request to {url}") def post( self, url: URL | str, json: Any | None = None, data: RequestData | None = None, files: RequestFiles | None = None ) -> Response: """ Выполняет POST-запрос. :param url: URL эндпоинта :param json: JSON тело запроса :param data: Данные формы :param files: Файлы для загрузки :return: HTTP-ответ """ return self.client.post(url, json=json, data=data, files=files) @allure.step("Make PATCH request to {url}") def patch(self, url: URL | str, json: Any | None = None) -> Response: """ Выполняет PATCH-запрос. :param url: URL эндпоинта :param json: JSON тело запроса :return: HTTP-ответ """ return self.client.patch(url, json=json) @allure.step("Make DELETE request to {url}") def delete(self, url: URL | str) -> Response: """ Выполняет DELETE-запрос. :param url: URL эндпоинта :return: HTTP-ответ """ return self.client.delete(url) def get_http_client(config: HTTPClientConfig) -> Client: """ Функция для инициализации HTTP-клиента. :param config: Конфигурация HTTP-клиента :return: Экземпляр httpx.Client """ return Client( timeout=config.timeout, base_url=config.client_url, event_hooks={ "request": [log_request_event_hook], # Логирование перед запросом "response": [log_response_event_hook] # Логирование после ответа } )
  • В BaseClient не изменялось поведение запросов – добавились только event hooks.
  • В get_http_client(config: HTTPClientConfig):event_hooks["request"] = [log_request_event_hook] → логируем запрос перед отправкой.event_hooks["response"] = [log_response_event_hook] → логируем ответ после получения.
  • Теперь при каждом запросе и ответе автоматически создаются записи в логах.
2025-03-30 13:38:06,646 | HTTP_CLIENT | INFO | Make GET request to https://api.sampleapis.com/fakebank/accounts/194 2025-03-30 13:38:07,023 | HTTP_CLIENT | INFO | Got response 200 OK from https://api.sampleapis.com/fakebank/accounts/194

Фикстуры

Реализуем фикстуры, необходимые для корректной и изолированной работы тестов. Нам понадобятся следующие фикстуры:

  • function_operation – создаёт новую операцию для каждого теста, чтобы её можно было удалить, обновить или получить в тесте.
  • operations_client – инициализирует и возвращает API-клиент для работы с операциями. Запускается перед каждым тестом.
  • settings – инициализирует настройки один раз на всю тестовую сессию.

Почему Pytest плагины, а не conftest.py?

Для объявления и управления фикстурами будем использовать Pytest плагины. Плагины удобнее и гибче, чем объявления фикстур в conftest.py, потому что:

  • Нет необходимости заботиться о расположении conftest.py.
  • Фикстуры из плагинов доступны глобально во всём проекте, вне зависимости от структуры тестов.
  • Использование conftest.py оправдано только для специфических групп тестов. В противном случае файлы conftest.py разрастаются до 1000+ строк, что затрудняет поддержку.
import pytest from config import Settings @pytest.fixture(scope="session") def settings() -> Settings: """ Фикстура создаёт объект с настройками один раз на всю тестовую сессию. :return: Экземпляр класса Settings с загруженными конфигурациями. """ return Settings()
  • @pytest.fixture(scope="session") – фиксирует, что настройка создаётся один раз за всю тестовую сессию.
  • settings() возвращает объект Settings, который можно переиспользовать в других фикстурах и тестах.
  • Использование этой фикстуры позволяет избежать повторной инициализации настроек в каждом тесте.
import pytest from clients.operations_client import OperationsClient, get_operations_client from config import Settings from schema.operations import OperationSchema @pytest.fixture def operations_client(settings: Settings) -> OperationsClient: """ Фикстура создаёт экземпляр API-клиента для работы с операциями. :param settings: Объект с настройками тестовой сессии. :return: Экземпляр OperationsClient. """ return get_operations_client(settings) @pytest.fixture def function_operation(operations_client: OperationsClient) -> OperationSchema: """ Фикстура создаёт тестовую операцию перед тестом и удаляет её после выполнения теста. :param operations_client: API-клиент для работы с операциями. :return: Созданная тестовая операция. """ operation = operations_client.create_operation() yield operation operations_client.delete_operation_api(operation.id)
  • operations_client(settings: Settings)Создаёт экземпляр API-клиента для работы с операциями.Использует настройки settings, передавая их в get_operations_client().
  • function_operation(operations_client: OperationsClient)Создаёт операцию перед тестом (operations_client.create_operation()).Передаёт её в тест через yield.Удаляет операцию после завершения теста (operations_client.delete_operation_api(operation.id)).

Почему function_operation, а не просто operation?

Рекомендую использовать принцип именования {scope}_{entity}, где:

  • function_ – указывает, что операция создаётся на уровне отдельного теста.
  • _operation – указывает, что это объект операции.

Если потребуется аналогичная фикстура с уровнем class, её можно назвать class_operation, без необходимости придумывать сложные названия.

Нужно ли удалять тестовые данные?

Удаление созданных данных оправдано, если тестовые данные не нужны в будущем. Однако, если:

  • Данные могут быть полезны для ручных проверок.
  • Их можно использовать для отладки ошибок.

то удалять их не стоит.

Добавляем файлы с фикстурами в корневой conftest.py, чтобы они подключались автоматически:

pytest_plugins = ( "fixtures.settings", "fixtures.operations" )

Теперь фикстуры settings, operations_client и function_operation будут доступны глобально во всех тестах.

Валидация JSON схемы

Зачем нужна валидация JSON-схемы?

При работе с API важно проверять, соответствует ли возвращаемый JSON-объект заранее определённому контракту. Это позволяет:

  • Глобально контролировать структуру данных.
  • Быстро выявлять изменения в API, которые могут сломать автотесты.
  • Убедиться, что запланированные изменения затрагивают нужные тесты.
  • Защититься от случайных изменений, когда разработчик допустил ошибку и API стало возвращать невалидные данные.

Чтобы реализовать такую проверку, будем использовать библиотеку jsonschema, которая позволяет валидировать JSON-объекты согласно заданным схемам.

Реализации функции валидации JSON-схемы

Создадим функцию, которая будет выполнять валидацию JSON-объекта по переданной JSON-схеме.

from typing import Any import allure from jsonschema import validate from jsonschema.validators import Draft202012Validator from tools.logger import get_logger # Логгер для записи информации о процессе валидации logger = get_logger("SCHEMA_ASSERTIONS") @allure.step("Validating JSON schema") def validate_json_schema(instance: Any, schema: dict) -> None: """" Проверяет, соответствует ли JSON-объект (instance) заданной JSON-схеме (schema). :param: instance (Any): JSON-объект, который необходимо проверить. :param: schema (dict): JSON-схема, согласно которой производится валидация. :raises: jsonschema.exceptions.ValidationError: В случае несоответствия JSON-объекта схеме. jsonschema.exceptions.SchemaError: Если переданная схема некорректна. """ logger.info("Validating JSON schema") # Выполняем валидацию JSON-объекта по заданной схеме validate( schema=schema, # JSON-схема, задающая структуру данных instance=instance, # Проверяемый JSON-объект format_checker=Draft202012Validator.FORMAT_CHECKER, # Проверка форматов (например, email, дата и т.д.) )
  1. Импорт необходимых модулейjsonschema.validate — функция для валидации JSON-объекта.Draft202012Validator.FORMAT_CHECKER — используется для проверки форматов (например, email, дата и т. д.).
  2. Определение функции validate_json_schemaПринимает JSON-объект (instance) и JSON-схему (schema).Логирует начало процесса валидации.Вызывает validate(), передавая объект и схему.
  3. Обработка ошибокЕсли объект не соответствует схеме, jsonschema выбросит ValidationError.Если сама схема содержит ошибки, будет вызван SchemaError.

Добавляя такую валидацию в автотесты, мы можем быстро находить проблемы с контрактами API и защищаться от неожиданных изменений. Это помогает поддерживать стабильность тестов и предотвращать регрессии.

Проверки

Зачем нужны проверки?

При тестировании API важно не только вызывать конечные точки, но и проверять, соответствуют ли полученные результаты ожидаемым. Для этого мы реализуем систему проверок.

Прежде чем добавлять проверки для конкретных операций, создадим базовые проверки, которые выполняют низкоуровневые операции, такие как сравнение значений и проверку статус-кода.

Зачем нужны базовые проверки?

  1. Отображение в Allure-отчётахВсе проверки будут видны как отдельные шаги в отчёте, что упростит анализ тестов.
  2. Логирование проверокКаждая проверка будет логироваться, что поможет в отладке тестов как локально, так и на CI.
  3. Снижение бойлерплейт-кодаВместо того чтобы каждый раз писать assert actual == expected, "Ошибка ...", мы будем использовать универсальные функции с уже готовыми сообщениями об ошибках.

Реализация базовых проверок

Создадим модуль содержащий две базовые проверки:

  • assert_status_code — проверяет, что статус-код ответа соответствует ожидаемому.
  • assert_equal — проверяет, что два значения равны.
from typing import Any import allure from tools.logger import get_logger logger = get_logger("BASE_ASSERTIONS") @allure.step("Check that response status code equals to {expected}") def assert_status_code(actual: int, expected: int): """ Проверяет, что HTTP-статус ответа соответствует ожидаемому. :param: actual (int): Фактический статус-код. :param: expected (int): Ожидаемый статус-код. :raises: AssertionError: Если статус-коды не совпадают. """ logger.info(f"Check that response status code equals to {expected}") assert actual == expected, ( f'Incorrect response status code. ' f'Expected status code: {expected}. ' f'Actual status code: {actual}' ) @allure.step("Check that {name} equals to {expected}") def assert_equal(actual: Any, expected: Any, name: str): """ Проверяет, что два значения равны. :param: actual (Any): Фактическое значение. :param: expected (Any): Ожидаемое значение. :param: name (str): Имя проверяемого параметра (для логирования). :raises: AssertionError: Если значения не равны. """ logger.info(f'Check that "{name}" equals to {expected}') assert actual == expected, ( f'Incorrect value: "{name}". ' f'Expected value: {expected}. ' f'Actual value: {actual}' )
  • Функция assert_status_codeСравнивает фактический и ожидаемый HTTP-статус ответа.Если статус-коды не совпадают — выдаёт AssertionError с подробным сообщением.
  • Функция assert_equalПроверяет, что два значения равны.Использует name для логирования, чтобы было понятно, что именно сравнивается.При несовпадении выдаёт AssertionError с детальным описанием ошибки.

Реализация проверок операций

Теперь реализуем проверки для операций. Они позволят убедиться, что API возвращает корректные данные при создании, обновлении и получении операций.

  1. assert_create_operation — проверяет, что API вернуло корректные данные после создания или обновления операции.
  2. assert_operation — полностью проверяет модель операции при её получении.
import allure from schema.operations import CreateOperationSchema, OperationSchema, UpdateOperationSchema from tools.assertions.base import assert_equal from tools.logger import get_logger logger = get_logger("OPERATIONS_ASSERTIONS") @allure.step("Check create operation") def assert_create_operation( actual: OperationSchema, expected: CreateOperationSchema | UpdateOperationSchema ): """ Проверяет, что данные, возвращённые API после создания/обновления операции, соответствуют ожидаемым. :param: actual (OperationSchema): Фактические данные операции. :param: expected (CreateOperationSchema | UpdateOperationSchema): Ожидаемые данные. :raises: AssertionError: Если значения полей не совпадают. """ logger.info("Check create operation") assert_equal(actual.debit, expected.debit, "debit") assert_equal(actual.credit, expected.credit, "credit") assert_equal(actual.category, expected.category, "category") assert_equal(actual.description, expected.description, "description") assert_equal(actual.transaction_date, expected.transaction_date, "transaction_date") @allure.step("Check operation") def assert_operation(actual: OperationSchema, expected: OperationSchema): logger.info("Check operation") assert_equal(actual.id, expected.id, "id") assert_equal(actual.debit, expected.debit, "debit") assert_equal(actual.credit, expected.credit, "credit") assert_equal(actual.category, expected.category, "category") assert_equal(actual.description, expected.description, "description") assert_equal(actual.transaction_date, expected.transaction_date, "transaction_date")
  • Функция assert_create_operationПроверяет корректность данных после создания или обновления операции.Сравнивает debit, credit, category, description и transaction_date.
  • Функция assert_operationПроверяет все поля операции, включая её id.Используется при тестировании получения операции.

API тесты

Теперь напишем API автотесты, используя:

  • API клиенты для отправки запросов.
  • Фикстуры для подготовки тестовых данных.
  • Проверки, реализованные ранее.
  • Валидацию JSON-схемы, чтобы убедиться, что ответы соответствуют ожидаемой структуре.

Всего у нас будет несколько тестов на базовые CRUD-операции.

from http import HTTPStatus # Используем enum HTTPStatus вместо магических чисел import allure import pytest from clients.operations_client import OperationsClient from schema.operations import OperationsSchema, OperationSchema, CreateOperationSchema, UpdateOperationSchema from tools.assertions.base import assert_status_code from tools.assertions.operations import assert_operation, assert_create_operation from tools.assertions.schema import validate_json_schema @pytest.mark.operations @pytest.mark.regression class TestOperations: @allure.title("Get operations") def test_get_operations(self, operations_client: OperationsClient): response = operations_client.get_operations_api() assert_status_code(response.status_code, HTTPStatus.OK) validate_json_schema(response.json(), OperationsSchema.model_json_schema()) @allure.title("Get operation") def test_get_operation( self, operations_client: OperationsClient, function_operation: OperationSchema ): response = operations_client.get_operation_api(function_operation.id) operation = OperationSchema.model_validate_json(response.text) assert_status_code(response.status_code, HTTPStatus.OK) assert_operation(operation, function_operation) validate_json_schema(response.json(), operation.model_json_schema()) @allure.title("Create operation") def test_create_operation(self, operations_client: OperationsClient): request = CreateOperationSchema() response = operations_client.create_operation_api(request) operation = OperationSchema.model_validate_json(response.text) assert_status_code(response.status_code, HTTPStatus.CREATED) assert_create_operation(operation, request) validate_json_schema(response.json(), operation.model_json_schema()) @allure.title("Update operation") def test_update_operation( self, operations_client: OperationsClient, function_operation: OperationSchema ): request = UpdateOperationSchema() response = operations_client.update_operation_api(function_operation.id, request) operation = OperationSchema.model_validate_json(response.text) assert_status_code(response.status_code, HTTPStatus.OK) assert_create_operation(operation, request) validate_json_schema(response.json(), operation.model_json_schema()) @allure.title("Delete operation") def test_delete_operation( self, operations_client: OperationsClient, function_operation: OperationSchema ): delete_response = operations_client.delete_operation_api(function_operation.id) assert_status_code(delete_response.status_code, HTTPStatus.OK) # Дополнительная проверка: убеждаемся, что операция действительно удалена get_response = operations_client.get_operation_api(function_operation.id) assert_status_code(get_response.status_code, HTTPStatus.NOT_FOUND)
  • Используем HTTPStatus вместо магических чиселВместо 200, 201, 404 и других кодов используем HTTPStatus.OK, HTTPStatus.CREATED, HTTPStatus.NOT_FOUND. Это делает код читаемым и исключает случайные ошибки.
  • Дополнительная проверка после удаленияПосле удаления операции выполняем запрос GET /fakebank/accounts/{id} и проверяем, что сервер вернул 404 Not Found. Это позволяет убедиться, что операция действительно удалена.
  • Читаемые заголовки тестов в Allure-отчетеИспользуем @allure.title(), чтобы тесты имели понятные названия в Allure.
  • Добавлены pytest маркировки для удобного запуска

Добавим pytest-маркировки в pytest.ini, чтобы избежать предупреждений при запуске.

[pytest] addopts = -s -v python_files = *_tests.py test_*.py python_classes = Test* python_functions = test_* markers = regression: Маркировка для регрессионных тестов. operations: Маркировка для тестов, связанных с операциями.

Благодаря правильно выбранной стратегии написания API автотестов, тесты сфокусированы на проверке бизнес-логики, а не на технических деталях.

Вместо того чтобы загромождать тесты шагами Allure, обработкой данных, валидацией JSON-схем и встроенными проверками, мы выносим эти задачи на другие уровни тестового фреймворка. Такой подход делает тесты:

  • Читаемыми – код остается лаконичным и интуитивно понятным.
  • Простыми в написании – добавление нового теста требует минимальных усилий.
  • Поддерживаемыми – изменения в API или проверках вносятся централизованно, а не в каждом тесте.

В результате тесты сфокусированы исключительно на бизнес-логике, а все вспомогательные процессы (логирование, шаги Allure, взаимодействие с API, выполнение проверок и их сообщения) скрыты на других уровнях фреймворка. Это делает API автотесты эффективными и удобными в работе.

Запуск на CI/CD

Настроим workflow-файл для автоматического запуска API-тестов в GitHub Actions, генерации Allure-отчета с сохранением истории и публикации его на GitHub Pages.

name: API tests # Название workflow on: push: branches: - main # Запускать workflow при пуше в main pull_request: branches: - main # Запускать workflow при открытии PR в main jobs: run-tests: # Джоба для запуска тестов runs-on: ubuntu-latest # Используем последнюю версию Ubuntu steps: - name: Check out repository # Клонирование кода репозитория в среду CI/CD uses: actions/checkout@v4 - name: Set up Python # Установка Python uses: actions/setup-python@v5 with: python-version: '3.12' # Используем Python версии 3.12 - name: Install dependencies # Установка зависимостей проекта run: | python -m pip install --upgrade pip # Обновляем pip до последней версии pip install -r requirements.txt # Устанавливаем зависимости, указанные в requirements.txt - name: Run API tests with pytest and generate Allure results # Запуск тестов run: | pytest -m regression --alluredir=allure-results --numprocesses 2 # Запускаем тесты с меткой "regression" # --alluredir=allure-results сохраняет результаты в папку allure-results # --numprocesses 2 - выполняем тесты в 2 потока (ускоряет выполнение) - name: Upload Allure results # Загружаем результаты тестов в GitHub Actions if: always() # Загружаем файлы независимо от успеха/неуспеха тестов uses: actions/upload-artifact@v4 with: name: allure-results # Название артефакта path: allure-results # Путь к файлам отчета publish-report: # Джоба для публикации Allure-отчета на GitHub Pages needs: [ run-tests ] # Выполняется только после успешного выполнения run-tests runs-on: ubuntu-latest # Используем последнюю версию Ubuntu steps: - name: Check out repository # Клонируем репозиторий, включая ветку gh-pages uses: actions/checkout@v4 with: ref: gh-pages # Операции будем выполнять в ветке gh-pages path: gh-pages # Клонируем файлы в папку gh-pages - name: Download Allure results # Загружаем ранее сохраненные результаты тестов uses: actions/download-artifact@v4 with: name: allure-results # Название артефакта path: allure-results # Путь для скачивания - name: Allure Report action from marketplace # Генерация отчета Allure uses: simple-elf/allure-report-action@v1.12 if: always() with: allure_results: allure-results # Папка с результатами тестов allure_history: allure-history # Папка для хранения истории отчетов - name: Deploy report to Github Pages # Публикация отчета на GitHub Pages if: always() uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} # Токен для доступа к репозиторию publish_branch: gh-pages # Публикуем отчет в ветку gh-pages publish_dir: allure-history # Папка, где находится сгенерированный отчет

Ссылки на документацию для всех использованных actions можно найти ниже:

Разрешения для Workflow

Если сейчас запустить тесты на GitHub Actions то, будет ошибка, говорящая о том, что у github token из workflow по умолчанию нет прав на записть в репзоиторий

Для исправления этой ошибки необходимо вручную изменить настройки прав workflow:

  1. Откройте вкладку Settings в репозитории GitHub.
API автотесты на Python с запуском на CI/CD и Allure отчетом

2. Перейдите в раздел Actions → General.

API автотесты на Python с запуском на CI/CD и Allure отчетом
  1. Прокрутите страницу вниз до блока Workflow permissions.
  2. Выберите опцию Read and write permissions.
  3. Нажмите кнопку Save для сохранения изменений.
API автотесты на Python с запуском на CI/CD и Allure отчетом

После выполнения этих шагов можно отправить код с API-тестами в удалённый репозиторий.

Запуск тестов и генерация Allure-отчёта

После коммита изменений во вкладке Actions появится новый workflow, в котором автоматически запустятся тесты.

API автотесты на Python с запуском на CI/CD и Allure отчетом

Если тесты пройдут успешно, Allure-отчёт будет сгенерирован и загружен в ветку gh-pages, после чего автоматически запустится workflow pages build and deployment. Этот процесс публикует Allure-отчёт на GitHub Pages, делая его доступным для просмотра в браузере.

Важно! Перед запуском workflow необходимо убедиться, что в репозитории существует ветка gh-pages. Если ветка отсутствует, её необходимо создать в удалённом репозитории, иначе публикация Allure-отчёта на GitHub Pages не будет работать.

Проверка настроек GitHub Pages

Если workflow pages build and deployment не запустился, необходимо проверить настройки GitHub Pages:

  1. Откройте вкладку Settings в репозитории.
  2. Перейдите в раздел Pages → Build and deployment.
  3. Убедитесь, что параметры соответствуют настройкам на скриншоте ниже.

На этой же странице будет отображаться виджет со ссылкой на опубликованный Allure-отчёт.

API автотесты на Python с запуском на CI/CD и Allure отчетом

Доступ к Allure-отчётам

  • Каждый отчёт публикуется на GitHub Pages с уникальным идентификатором workflow, в котором он был сгенерирован.
  • Все сгенерированные Allure-отчёты также можно найти в ветке gh-pages.
  • Перейдя по ссылке на GitHub Pages, можно открыть сгенерированный Allure-отчёт с историей результатов тестирования.
API автотесты на Python с запуском на CI/CD и Allure отчетом

По итогу, после корректной настройки, при каждом новом запуске тестов Allure-отчёт будет автоматически обновляться и сохранять историю предыдущих прогонов.

API автотесты на Python с запуском на CI/CD и Allure отчетом

Заключение

Все ссылки на код, отчеты и запуски тестов в CI/CD можно найти на моем GitHub:

Начать дискуссию