Привет, Хабр! Меня зовут Вадим, я уже много лет в тестировании и сейчас работаю 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-инженерам.
Lizalush13
какая топовая статья! с кодом с полным объяснением: рассмотренные варианты. просто кладезь.
Спасибо большое автору!
<3 сохраняю себе