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