Привет, Хабр! Проблема рассинхронизации автотестов и тестовой документации знакома многим. Код постоянно меняется, а кейсы в Confluence — нет. В итоге документация становится бесполезной, а время команды тратится на выяснение того, что же на самом деле проверяет тот или иной тест.
Есть занятия, которые наполняют жизнь QA-инженера особым, экзистенциальным смыслом, и ручное ведение тест-кейсов, бесспорно, одно из них. Этот медитативный ритуал — найти нужную страницу в Confluence, сверить её с кодом, осознать их полную асинхронность, глубоко вздохнуть и начать творить — несравненно закаляет дух. Но, увы, в какой-то момент безжалостные требования бизнеса к скорости заставили меня пожертвовать этим священным процессом и, скрепя сердце, написать скрипт, который делает всю эту замечательную работу за меня.
Идея генерировать документацию из кода тестов не нова, и на Хабре можно найти немало успешных примеров ее реализации. Проанализировав существующие подходы, я поставил себе цель — добиться того же результата, но с минимальным вмешательством в код самих тестов. Моё решение — это комбинация чистого бизнес-ориентированного синтаксиса в тестах и простого, но мощного генератора, который парсит шаги Allure. Вот как это работает.
Основная цель
Я хочу, чтобы такой лаконичный тест, лишенный деталей реализации Allure:
# tests/test_users.py
import allure
from schemas.user import UserSchema
@allure.title("Успешное получение данных пользователя")
class TestGetUser:
def test_get_user_by_id(self, api_client, Assertions):
# Код теста — это чистый бизнес-сценарий
response = api_client.get_user(user_id=2)
Assertions.assert_status_code(response, 200)
Assertions.assert_pydantic_schema(response, UserSchema)
Assertions.assert_json_value_by_name(response, "first_name", "Janet")
...автоматически, после каждого запуска, превращался вот в такую понятную документацию.
Что получается на выходе: Примеры тест-кейсов
Вот как выглядят .md
файлы, которые генерирует наша система.
Пример 1: GET-запрос
### Успешное получение данных пользователя
**1. Отправить GET запрос на https://reqres.in/api/users/2**
**2. Проверка статус-кода. ОР: 200**
**3. Проверка соответствия ответа схеме Pydantic 'UserSchema'. ОР: Успешная валидация**
**4. Проверка значения ключа 'first_name'. ОР: 'Janet'**
---
Пример 2: POST-запрос с телом
### Успешное создание пользователя
**1. Отправить POST запрос на https://reqres.in/api/users с телом: {'name': 'morpheus', 'job': 'leader'}**
**2. Проверка статус-кода. ОР: 201**
**3. Проверка значения ключа 'name'. ОР: 'morpheus'**
---
Документация получается подробной и всегда на 100% соответствующей коду. Теперь давайте разберем, как это устроено под капотом.
Шаг 1: «Умный» API-клиент — сердце системы
Главная магия происходит в кастомном классе для отправки запросов. Вместо того чтобы прятать данные в скрытые аттачменты, делаем их проще и надежнее: вся необходимая для генератора информация встраивается прямо в название шага Allure.
# helpers/api_requests.py
import allure
import requests
import json
class ApiRequests:
@staticmethod
def post(url: str, data: dict = None, headers: dict = None, cookies: dict = None):
return ApiRequests._send(url, data, headers, cookies, "POST")
@staticmethod
def get(url: str, data: dict = None, headers: dict = None, cookies: dict = None):
return ApiRequests._send(url, data, headers, cookies, "GET")
@staticmethod
def _send(url: str, data: dict, headers: dict, cookies: dict, method: str):
# ФОРМИРУЕМ НАШУ СПЕЦИАЛЬНУЮ СТРОКУ ДЛЯ ALLURE
# Она содержит все, что нужно знать генератору: метод, URL и тело.
with allure.step(f"{method} {url}, Request Body: {data}"):
if headers is None: headers = {}
if cookies is None: cookies = {}
# Для POST/PUT/PATCH запросов используем `json=data`,
# чтобы автоматически устанавливать правильный Content-Type.
if method in ['POST', 'PUT', 'PATCH']:
r = requests.request(method, url, json=data, headers=headers, cookies=cookies)
else: # Для GET, DELETE и других передаем данные как параметры URL
r = requests.request(method, url, params=data, headers=headers, cookies=cookies)
return r
Эта простая строка f"{method} {url}, Request Body: {data}"
— это контракт. Генератор будет знать, как её разобрать.
Шаг 2: «Умные» ассерты — строительные блоки
Вторая важная часть — это хелпер для проверок. Здесь также прячем логику allure.step
внутрь, используя формат «Проверка X. ОР: Y».
# helpers/assertions.py
import allure
from requests import Response
class Assertions:
@staticmethod
def assert_status_code(response: Response, code: int):
"""Проверяет, что статус-код ответа равен ожидаемому."""
with allure.step(f"Проверка статус-кода. ОР: {code}"):
assert response.status_code == code, \
f"Неверный статус-код. Ожидался: {code}, фактический: {response.status_code}"
@staticmethod
def assert_json_value_by_name(response_or_dict, name: str, expected_value: any, step_name: str = None):
"""Проверяет значение в JSON по ключу."""
description = step_name or f"Проверка значения ключа '{name}'"
with allure.step(f"{description}. ОР: '{expected_value}'"):
response_as_dict = Assertions._get_json(response_or_dict)
assert name in response_as_dict, f"Ключ '{name}' отсутствует в JSON ответе."
actual_value = response_as_dict.get(name)
assert actual_value == expected_value, \
f"Неверное значение для ключа '{name}'. Ожидалось: '{expected_value}', фактическое: '{actual_value}'"
@staticmethod
def assert_pydantic_schema(response_or_dict, schema: BaseModel):
"""Проверяет соответствие JSON ответа Pydantic-схеме."""
with allure.step(f"Проверка соответствия ответа схеме Pydantic '{schema.__name__}'. ОР: Успешная валидация"):
json_to_validate = Assertions._get_json(response_or_dict)
try:
schema.model_validate(json_to_validate)
except ValidationError as e:
allure.attach(body=e.json(), name="Pydantic validation error", attachment_type=allure.attachment_type.JSON)
raise AssertionError(f"JSON не соответствует схеме '{schema.__name__}':\n{e}")
Шаг 3: Генератор на Regex — двигатель системы
Теперь самое интересное — скрипт, который парсит отчеты Allure. Он использует регулярное выражение, чтобы находить и разбирать специально отформатированные шаги с запросами.
# generate_test_cases.py
import os
import json
import pathlib
import re
ALLURE_RESULTS_DIR = 'allure_results'
TEST_CASES_OUTPUT_DIR = 'test_cases'
def generate_test_cases():
results_path = pathlib.Path(ALLURE_RESULTS_DIR)
output_path = pathlib.Path(TEST_CASES_OUTPUT_DIR)
if not results_path.is_dir():
print(f"Директория '{ALLURE_RESULTS_DIR}' не найдена.")
return
output_path.mkdir(exist_ok=True)
# Регулярное выражение для парсинга шага с запросом
request_pattern = re.compile(
r"^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.*?),\s+Request Body:\s+(.*)$"
)
for result_file in results_path.glob('*-result.json'):
with open(result_file, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'name' not in data or 'steps' not in data:
continue
test_case_title = next((label['value'] for label in data.get('labels', []) if label['name'] == 'title'), data['name'])
md_content = f"### {test_case_title}\n\n"
for i, step in enumerate(data['steps'], 1):
step_name = step.get('name', 'Шаг без названия')
match = request_pattern.match(step_name)
if match:
method, url, body_str = match.groups()
step_text = f"Отправить {method} запрос на {url}"
if body_str != 'None':
step_text += f" с телом: {body_str}"
md_content += f"**{i}. {step_text}**\n"
else:
md_content += f"**{i}. {step_name}**\n"
test_file_name = data.get('fullName', data['name']).split('.')[-2].replace("test_", "")
output_file = output_path / f"{test_file_name}.md"
with open(output_file, 'a', encoding='utf-8') as f:
f.write(md_content + "\n---\n")
if __name__ == "__main__":
generate_test_cases()
Заключение
Этот подход позволил мне достичь главного — создать единый источник правды. Код автотеста является и исполняемой проверкой, и шаблоном для документации.
Ключевые преимущества этого решения:
Чистый код тестов: Тесты описывают бизнес-логику, а не детали отчетности.
Надежность: Парсинг строки с помощью Regex прост и устойчив.
Актуальность: Документация генерируется после каждого запуска и никогда не устаревает.
Прозрачность: Любой член команды может понять, что делает тест, просто прочитав
.md
файл.
При желании, можно пойти дальше — добавить интеграцию с Jira, что бы кейсы обновлялись автоматически, после каждого запуска. Немного изменить генератор тестов, что бы он подготавливал данные, подходящие для взаимодействия с API Jira. Для меня это пока что далекий беклог, и возможно когда-то я это реализую, но сейчас мне лень :)
Надеюсь, этот подробный гайд поможет вам внедрить подобную систему у себя и навсегда забыть о боли ручного ведения тестовой документации.
Делитесь мнением об этой реализации в комментариях :)