Автотесты как документация: «чистый» код и генератор на Regex

от автора

Привет, Хабр! Проблема рассинхронизации автотестов и тестовой документации знакома многим. Код постоянно меняется, а кейсы в 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% Круто, попробую внедрить у себя что-то похожее!0
0% Отличная идея, покажу команде/тимлиду.0
0% Интересно, но для нас пока слишком сложно.0
0% Мы уже используем подобный подход.0
100% Просто было интересно почитать.1

Проголосовал 1 пользователь. Воздержавшихся нет.

ссылка на оригинал статьи https://habr.com/ru/articles/930908/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *