Привет, Хабр! Проблема рассинхронизации автотестов и тестовой документации знакома многим. Код постоянно меняется, а кейсы в 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. Для меня это пока что далекий беклог, и возможно когда-то я это реализую, но сейчас мне лень :)

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

Делитесь мнением об этой реализации в комментариях :)

Комментарии (0)