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, для проверки корректности данных
Обеспечивает удобную типизацию
Мы определим:
CreateOperationSchema
– модель для создания новой операцииUpdateOperationSchema
– модель для обновления данных (используется для PATCH запросов)OperationSchema
– расширенная модель с id, представляющая конечную структуру операции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
Класс
Settings
Наследуется от BaseSettings (Pydantic Settings).
model_config
определяет:Где искать переменные (из файла .env).
Кодировку файла (utf-8).
Поддержку вложенных переменных
(env_nested_delimiter='.')
.
fake_bank_http_client: HTTPClientConfig
— добавляет вложенные настройки для HTTP-клиента.
.env
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
.
Шаги для BaseClient: На уровне
BaseClient
шаги содержат подробную техническую информацию о том, какой HTTP-метод использовался (например, GET, POST, PATCH, DELETE), куда был отправлен запрос, с каким телом и параметрами. Это позволяет нам точно видеть все детали запроса, которые были отправлены на сервер. Например, мы можем узнать:Используемый HTTP-метод.
URL, по которому был отправлен запрос.
Тело запроса (если оно было).
Шаги для OperationsClient: На уровне
OperationsClient
шаги отражают описание выполняемых действий с точки зрения бизнес-логики. Здесь не отображаются технические детали (например, сам HTTP-запрос), а скорее бизнесовые действия, такие как "Получение списка операций", "Создание новой операции" и т.д. Это делает отчет Allure более понятным и ориентированным на бизнес-логику, не перегружая его техническими деталями.В случае, если нужно получить более детальную информацию (например, какие параметры были переданы в запросе или какие методы использовались), можно раскрыть шаги с детальным описанием, где будут представлены все технические детали.
Декоратор @allure.step: Важно отметить, что мы используем декоратор @allure.step специально, чтобы в отчет автоматически прикреплялись все параметры, передаваемые в методы и функции. Это позволяет нам в отчете видеть не только, какие шаги были выполнены, но и какие данные были переданы в каждом запросе, обеспечивая полную прозрачность всех действий.
Таким образом, с помощью такого подхода мы достигаем:
Гибкости в отчете, где можно раскрывать нужные детали on-demand.
Чистоты и понятности отчета, где бизнес-логику и технические детали можно разделить.
Логирование взаимодействий с 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, дата и т.д.)
)
Импорт необходимых модулей
jsonschema.validate — функция для валидации JSON-объекта.
Draft202012Validator.FORMAT_CHECKER — используется для проверки форматов (например, email, дата и т. д.).
Определение функции
validate_json_schema
Принимает JSON-объект (instance) и JSON-схему (schema).
Логирует начало процесса валидации.
Вызывает
validate()
, передавая объект и схему.
Обработка ошибок
Если объект не соответствует схеме, jsonschema выбросит ValidationError.
Если сама схема содержит ошибки, будет вызван SchemaError.
Добавляя такую валидацию в автотесты, мы можем быстро находить проблемы с контрактами API и защищаться от неожиданных изменений. Это помогает поддерживать стабильность тестов и предотвращать регрессии.
Проверки
Зачем нужны проверки?
При тестировании API важно не только вызывать конечные точки, но и проверять, соответствуют ли полученные результаты ожидаемым. Для этого мы реализуем систему проверок.
Прежде чем добавлять проверки для конкретных операций, создадим базовые проверки, которые выполняют низкоуровневые операции, такие как сравнение значений и проверку статус-кода.
Зачем нужны базовые проверки?
Отображение в Allure-отчётах
Все проверки будут видны как отдельные шаги в отчёте, что упростит анализ тестов.
Логирование проверок
Каждая проверка будет логироваться, что поможет в отладке тестов как локально, так и на CI.
Снижение бойлерплейт-кода
Вместо того чтобы каждый раз писать
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 возвращает корректные данные при создании, обновлении и получении операций.
assert_create_operation
— проверяет, что API вернуло корректные данные после создания или обновления операции.assert_operation
— полностью проверяет модель операции при её получении.
/tools/assertions/operations.py
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:
Откройте вкладку Settings в репозитории GitHub.
Перейдите в раздел Actions → General.
Прокрутите страницу вниз до блока Workflow permissions.
Выберите опцию Read and write permissions.
Нажмите кнопку Save для сохранения изменений.
После выполнения этих шагов можно отправить код с API-тестами в удалённый репозиторий.
Запуск тестов и генерация Allure-отчёта
После коммита изменений во вкладке Actions появится новый workflow, в котором автоматически запустятся тесты.
Если тесты пройдут успешно, 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:
Откройте вкладку Settings в репозитории.
Перейдите в раздел Pages → Build and deployment.
Убедитесь, что параметры соответствуют настройкам на скриншоте ниже.
На этой же странице будет отображаться виджет со ссылкой на опубликованный Allure-отчёт.
Доступ к Allure-отчётам
Каждый отчёт публикуется на GitHub Pages с уникальным идентификатором workflow, в котором он был сгенерирован.
Все сгенерированные Allure-отчёты также можно найти в ветке gh-pages.
Перейдя по ссылке на GitHub Pages, можно открыть сгенерированный Allure-отчёт с историей результатов тестирования.
По итогу, после корректной настройки, при каждом новом запуске тестов Allure-отчёт будет автоматически обновляться и сохранять историю предыдущих прогонов.
Заключение
Все ссылки на код, отчеты и запуски тестов в CI/CD можно найти на моем GitHub: