Привет, Хабр! Меня зовут Вадим, я уже много лет в тестировании и сейчас работаю Head of QA в Альфа-Банке (Беларусь). За эти годы я успел поработать с десятками инструментов, написать сотни тест-кейсов и... потратить неприлично много времени на рутину, которую можно было автоматизировать ещё вчера.
Знаете, есть такая особенность нашей профессии - мы автоматизируем всё вокруг, но часто забываем автоматизировать собственную боль. Сегодня хочу поделиться решением одной из таких "болей", с которой сталкивается каждый QA-инженер, работающий с ТестОпс: необходимость вручную синхронизировать тест-кейсы после каждого прогона автотестов.

Да-да, именно та ситуация, когда ты полчаса подряд кликаешь по кнопке «Синхронизировать». И это в 2025 году, когда ИИ уже пишет код лучше некоторых разработчиков!
Проблема: когда тест-кейсы внезапно "исчезают"
А теперь к сути проблемы. Представьте ситуацию: к вам приходит DPO (Director Product Owner) или PM (Product Manager) с вопросом по покрытию тестами какой-то функциональности. Вы заходите в ТестОпс, открываете тест-кейсы и... видите пустые сценарии. Хотя буквально пару дней назад всё было на месте, и тесты исправно выполнялись.

Знакомо? Мне - очень. Первый раз столкнувшись с этим, я подумал, что это какой-то глюк системы. Но после небольшого расследования выяснилось, что всё дело в политиках очистки данных, которые настроены в ТестОпс для предотвращения засорения базы данных.
Логика простая: старые результаты тестов удаляются через определённое время, а вместе с ними исчезают и связанные с ними шаги тест-кейсов. В итоге получается парадоксальная ситуация - тесты работают, резу��ьтаты есть, а сценарии в тест-кейсах пропали.
Варианты решения проблемы
Когда мы столкнулись с этой проблемой в банке, я проанализировал несколько возможных подходов: 1. Увеличить время жизни данных в политике очистки
✅ Простое решение
❌ Увеличивает нагрузку на БД
❌ Не решает проблему кардинально, просто откладывает её
2. Вручную синхронизировать каждый тест-кейс после прогона
✅ Гарантированный результат
❌ Огромные временные затраты
❌ Человеческий фактор (можно забыть)
❌ Не масштабируется
3. Настроить CI/CD так, чтобы тесты запускались чаще политики очистки
✅ Автоматическое решение
❌ Не всегда целесообразно (лишняя нагрузка на инфраструктуру)
❌ Подходит не для всех проектов
4. Автоматизировать процесс синхронизации через скрипт
✅ Эффективно и быстро
✅ Запускается по требованию
✅ Не создаёт лишней нагрузки
✅ Полностью решает проблему
Выбор был очевиден - четвёртый вариант. Именно поэтому родился этот скрипт.
Решение: автоматизируем всё
Как говорится, "если что-то делаешь больше двух раз - пора автоматизировать". А тут мы делали это сотни раз!
Поэтому мы с коллегой Сергеем (имя, возможно, изменено) написали Python-скрипт, который автоматически синхронизирует все тест-кейсы одной командой. Вернее, основной код написал Сергей, а я доработал. Никаких лишних кликов в интерфейсе, никакой рутины, просто запускаешь скрипт и идёшь пить кофе (или заниматься действительно важными задачами).
Самое приятное в этом решении - оно освобождает не только моё время, но и время всей команды. Теперь, вместо того чтобы тратить часы на механические клики, мы можем сосредоточиться на том, что действительно важно: анализе результатов тестирования, улучшении покрытия и поиске реальных багов.
Создаём sync_test_cases.py
import logging import os import sys from typing import Dict, List, Optional import requests from dotenv import load_dotenv class AllureTestCaseSyncer: """Класс для синхронизации тест-кейсов с результатами запусков в Allure TestOps.""" def __init__(self): """Инициализация синхронизатора с загрузкой конфигурации.""" load_dotenv() self._setup_logging() self._load_config() self._setup_headers() self.access_token: Optional[str] = None def _setup_logging(self) -> None: """Настройка логирования.""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler('allure_sync.log', encoding='utf-8') ] ) self.logger = logging.getLogger(__name__) def _load_config(self) -> None: """Загрузка и валидация конфигурации из переменных окружения.""" self.allure_url = os.getenv('ALLURE_URL') self.allure_token = os.getenv('ALLURE_TOKEN') self.launch_id = os.getenv('LAUNCH_ID') self.page_size = int(os.getenv('PAGE_SIZE', '1000')) # Валидация обязательных параметров if not self.allure_token: raise ValueError("ALLURE_TOKEN не установлен в переменных окружения") if not self.launch_id: raise ValueError("LAUNCH_ID не установлен в переменных окружения") self.logger.info(f"Конфигурация загружена: URL={self.allure_url}, LAUNCH_ID={self.launch_id}") def _setup_headers(self) -> None: """Настройка заголовков для HTTP запросов.""" self.json_headers = {'Accept': 'application/json'} self.all_headers = {'Accept': '*/*'} def authenticate(self) -> None: """Аутентификация в Allure TestOps и получение access token.""" self.logger.info("Начало аутентификации...") auth_data = { 'grant_type': (None, 'apitoken'), 'scope': (None, 'openid'), 'token': (None, self.allure_token), } try: response = requests.post( f'{self.allure_url}/api/uaa/oauth/token', headers=self.json_headers, files=auth_data, timeout=30 ) response.raise_for_status() self.access_token = response.json()['access_token'] self.auth_headers = {'Authorization': f'Bearer {self.access_token}'} self.logger.info("Аутентификация успешно завершена") except requests.exceptions.RequestException as e: self.logger.error(f"Ошибка аутентификации: {e}") raise except KeyError: self.logger.error("Неверный формат ответа при аутентификации") raise def close_launch(self) -> None: """Закрытие запуска в Allure TestOps.""" self.logger.info(f"Закрытие запуска {self.launch_id}...") try: response = requests.post( f'{self.allure_url}/api/launch/{self.launch_id}/close', headers=self.auth_headers, timeout=30 ) response.raise_for_status() self.logger.info(f"Запуск {self.launch_id} успешно закрыт") except requests.exceptions.RequestException as e: self.logger.warning(f"Ошибка при закрытии запуска: {e}") def get_test_results(self) -> List[Dict]: """Получение результатов тестов из запуска.""" self.logger.info(f"Получение информации о запуске {self.launch_id}...") params = { 'page': '0', 'size': str(self.page_size), 'sort': 'name,ASC', } try: response = requests.get( f'{self.allure_url}/api/v2/launch/{self.launch_id}/test-result/flat', params=params, headers={**self.auth_headers, **self.all_headers}, timeout=60 ) response.raise_for_status() content = response.json().get('content', []) self.logger.info(f"Получено {len(content)} результатов тестов") return content except requests.exceptions.RequestException as e: self.logger.error(f"Ошибка получения результатов тестов: {e}") raise def sync_test_case_scenario(self, test_case_id: str) -> bool: """ Синхронизация сценария для конкретного тест-кейса. Args: test_case_id: ID тест-кейса для синхронизации Returns: bool: True если синхронизация успешна, False в противном случае """ try: # Получение сценария из результатов выполнения self.logger.info(f"Получение сценария для тест-кейса {test_case_id}...") scenario_response = requests.get( f'{self.allure_url}/api/testcase/{test_case_id}/scenariofromrun', headers=self.auth_headers, timeout=30 ) scenario_response.raise_for_status() scenario_data = scenario_response.json() # Сохранение сценария в тест-кейс self.logger.info(f"Сохранение сценария для тест-кейса {test_case_id}...") save_response = requests.post( f'{self.allure_url}/api/testcase/{test_case_id}/scenario', headers={**self.auth_headers, **self.json_headers}, json=scenario_data, timeout=30 ) save_response.raise_for_status() self.logger.info(f"Сценарий для тест-кейса {test_case_id} успешно синхронизирован") return True except requests.exceptions.RequestException as e: self.logger.error(f"Ошибка синхронизации тест-кейса {test_case_id}: {e}") return False def sync_all_test_cases(self, test_results: List[Dict]) -> None: """Синхронизация всех тест-кейсов из переданных результатов тестов.""" if not test_results: self.logger.warning("Результаты тестов не найдены") return # Синхронизация каждого тест-кейса success_count = 0 total_count = len(test_results) for test_result in test_results: execution_id = test_result.get('id') test_case_id = test_result.get('testCaseId') if not test_case_id: self.logger.warning(f"Тест-кейс ID не найден для execution {execution_id}") continue self.logger.info(f"Обработка: ExecutionId={execution_id}, TestCaseId={test_case_id}") if self.sync_test_case_scenario(test_case_id): success_count += 1 self.logger.info(f"Синхронизация завершена: {success_count}/{total_count} успешно") def main() -> None: """Главная функция для запуска синхронизации.""" try: syncer = AllureTestCaseSyncer() # Аутентификация syncer.authenticate() # Закрытие запуска syncer.close_launch() # Получение результатов тестов test_results = syncer.get_test_results() # Синхронизация тест-кейсов syncer.sync_all_test_cases(test_results) except Exception as e: logging.error(f"Ошибка выполнения скрипта: {e}") sys.exit(1) if __name__ == "__main__": main()
Создаём файл .env:
# URL вашего ТестОпс инстанса (обязательно) ALLURE_URL=https://allure.example.com # API токен для доступа к ТестОпс (обязательно) # Получить можно в настройках профиля -> API tokens ALLURE_TOKEN=your_api_token_here # ID запуска (launch) для синхронизации (обязательно) LAUNCH_ID=12345 # Размер страницы для получения результатов (опционально, по умолчанию 1000) PAGE_SIZE=1000
Когда скрипт не нужен?
Важное уточнение: скрипт может быть не нужен, если ваша команда уже решила проблему политик очистки одним из альтернативных способов.
Скрипт НЕ нужен, если:
Тесты выполняются чаще, чем срабатывает политика очистки - результаты постоянно обновляются
Увеличили время жизни данных в политиках - и вас устраивает нагрузка на БД
Малое количество тест-кейсов (< 10-20) - проще синхронизировать вручную
Не используете связку тест-кейсов с результатами - работаете только с автотестами
Скрипт особенно полезен, если:
Тесты запускаются периодически - большие перерывы между прогонами
Много тест-кейсов - сотни тест-кейсов для синхронизации
Работа с заказчиками - DPO, PM регулярно просматривают тест-кейсы
Разные команды и проекты - нужно массово обновлять покрытие
Банковская/финансовая сфера - высокие требования к документированию процессов
Как это работает изнутри
Алгоритм простой и понятный:
def main() -> None: """Главная функция для запуска синхронизации.""" try: syncer = AllureTestCaseSyncer() # Аутентификация syncer.authenticate() # Закрытие запуска syncer.close_launch() # Получение результатов тестов test_results = syncer.get_test_results() # Синхронизация тест-кейсов syncer.sync_all_test_cases(test_results) except Exception as e: logging.error(f"Ошибка выполнения скрипта: {e}") sys.exit(1)
Скрипт использует официальное API ТестОпс, поэтому он надёжен и безопасен.
Запуск
python3 sync_test_cases.py
Всё! Скрипт автоматически синхронизирует все тест-кейсы из указанного запуска.
Пример использования с Playwright (JS/TS)
Особенно актуально для команд, использующих Playwright для автоматизации. Вот типичный workflow: 1. Настройка Playwright тестов
// playwright.config.js import { defineConfig } from '@playwright/test'; export default defineConfig({ reporter: [ ['html'], ['allure-playwright', { detail: true, outputFolder: 'allure-results', suiteTitle: false, }] ], // ... остальные настройки });
2. Пример теста с аннотациями
// tests/login.spec.ts import { test, expect } from '@playwright/test'; import { allure } from 'allure-playwright'; test.describe('Авторизация пользователя', () => { test('Успешная авторизация с валидными данными', async ({ page }) => { await allure.epic('Авторизация'); await allure.feature('Логин'); await allure.story('Позитивные сценарии'); await allure.step('Открываем страницу логина', async () => { await page.goto('/login'); await expect(page.locator('h1')).toContainText('Вход в систему'); }); await allure.step('Вводим валидные учётные данные', async () => { await page.fill('[data-testid=username]', 'testuser@example.com'); await page.fill('[data-testid=password]', 'SecurePassword123'); }); await allure.step('Нажимаем кнопку "Войти"', async () => { await page.click('[data-testid=login-button]'); }); await allure.step('Проверяем успешную авторизацию', async () => { await expect(page.locator('[data-testid=user-menu]')).toBeVisible(); await expect(page.url()).toContain('/dashboard'); }); }); });
3. Интеграция с CI/CD
# .github/workflows/tests.yml name: E2E Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - name: Generate Allure report run: npx allure generate allure-results --clean - name: Upload to ТестОпс run: | # Загрузка результатов в ТестОпс curl -X POST "${{ secrets.ALLURE_URL }}/api/result" \ -H "Authorization: Bearer ${{ secrets.ALLURE_TOKEN }}" \ -F "results=@allure-results.zip" - name: Sync test cases run: | # Получаем ID последнего запуска и синхронизируем export LAUNCH_ID=$(curl -s "${{ secrets.ALLURE_URL }}/api/launch" \ -H "Authorization: Bearer ${{ secrets.ALLURE_TOKEN }}" | \ jq -r '.content[0].id') python3 sync_test_cases.py env: ALLURE_URL: ${{ secrets.ALLURE_URL }} ALLURE_TOKEN: ${{ secrets.ALLURE_TOKEN }}
4. Результат

После выполнения пайплайна:
Тесты выполнены
Результаты загружены в ТестОпс
Тест-кейсы автоматически синхронизированы
Никаких ручных действий не требуется!

Логирование и мониторинг
Скрипт ведёт подробные логи, что помогает отслеживать процесс:
2025-09-15 14:30:01 - INFO - Конфигурация загружена: URL=https://allure.company.com, LAUNCH_ID=12345 2025-09-15 14:30:02 - INFO - Начало аутентификации... 2025-09-15 14:30:03 - INFO - Аутентификация успешно завершена 2025-09-15 14:30:04 - INFO - Закрытие запуска 12345... 2025-09-15 14:30:05 - INFO - Запуск 12345 успешно закрыт 2025-09-15 14:30:06 - INFO - Получение информации о запуске 12345... 2025-09-15 14:30:07 - INFO - Получено 47 результатов тестов 2025-09-15 14:30:08 - INFO - Обработка: ExecutionId=67890, TestCaseId=123 2025-09-15 14:30:09 - INFO - Сценарий для тест-кейса 123 успешно синхронизирован 2025-09-15 14:32:15 - INFO - Синхронизация завершена: 45/47 успешно
Обработка ошибок
Скрипт устойчив к ошибкам - если какой-то тест-кейс не удалось синхронизировать, остальные продолжают обрабатываться:
2025-09-15 14:31:30 - ERROR - Ошибка синхронизации тест-кейса 456: 403 Forbidden 2025-09-15 14:31:31 - INFO - Обработка: ExecutionId=67891, TestCaseId=457 2025-09-15 14:31:32 - INFO - Сценарий для тест-кейса 457 успешно синхронизирован
Альтернативные сценарии использования
Локальная разработка
# Запустили тесты локально npm run test:e2e # Получили LAUNCH_ID из ТестОпс export LAUNCH_ID=54321 # Синхронизировали тест-кейсы python3 sync_test_cases.py
Batch-обработка
# Синхронизация нескольких запусков for launch_id in 12345 12346 12347; do export LAUNCH_ID=$launch_id python3 sync_test_cases.py echo "Launch $launch_id synced" done
Если у вас релизы раз в неделю, то за год вы сэкономите около 40 часов рабочего времени. Это почти целая рабочая неделя! Представляете, что можно сделать за эту неделю вместо бездумного кликанья по кнопкам?
В масштабах нашей команды в Альфа-Банке экономия получается ещё более впечатляющей. У нас несколько команд QA, и каждая экономит десятки часов в месяц. Эти часы мы теперь тратим на улучшение процессов, обучение команды и, что самое важное, на повышение качества наших продуктов.
Заключение
За годы работы в тестировании я понял одну простую истину: если ты тратишь время на то, что можно автоматизировать, ты крадёшь это время у более важных задач. Автоматизация рутины - это не просто техническое решение, это философия эффективности.
Скрипт для синхронизации тест-кейсов в ТестОпс - это маленький, но важный шаг к тому, чтобы сделать работу QA-инженеров более осмысленной и продуктивной. Вместо механического кликанья мы можем заниматься тем, что действительно важно: думать, анализировать, улучшать.
Когда использовать:
✅ Периодические запуски тестов
✅ Большое количество тест-кейсов
✅ Интеграция с CI/CD
✅ Командная работа
Когда можно обойтись без скрипта:
❌ Тесты выполняются постоянно (каждый коммит)
❌ Малое количество тест-кейсов (< 10)
❌ Результаты тестов не связаны с тест-кейсами
А у вас есть опыт автоматизации работы с ТестОпс? Поделитесь в комментариях своими лайфхаками!
👉 Если тема автоматизации, тестирования и ИИ вам интересна - подписывайтесь на мой телеграм-канал https://t.me/it_vadimqa. Там я делюсь практикой, кейсами и инструментами, которые реально упрощают жизнь QA-инженерам.
