Привет, Хабр! Меня зовут Вадим Лунин. Я — QA Manager из Альфа Банка в Беларуси.
Поделюсь своим опытом решения одной проблемы, с которой мы столкнулись в связке Allure TestOps + Playwright.
Итак, на одном из проектов мы активно пишем тест-кейсы в Allure TestOps (на текущий момент их почти 2000) и автоматизируем их, используя Playwright (TypeScript). Около 1400 тест-кейсов уже удалось автоматизировать. И вот неожиданно мы заметили, что объём наших тест-кейсов начал увеличиваться в геометрической прогрессии. Проанализировав весь процесс, мы выяснили, что виной всему - генерация дублирующих тест-кейсов в процессе прогона.

Для удобства восприятия и систематизации я разделю свой материал на несколько частей:
Инструментарий
Как работает Allure TestOps
Test case ID
History ID
Описание проблемы
Решение
Немного про не пройденные тесты
Инструментарий
Allure TestOps - это полнофункциональное управление тестированием, ориентированное на автоматизацию в соответствии с DevOps практиками.
Playwright (TypeScript) — это библиотека Node.js для автоматизации Chromium, Firefox и WebKit с помощью одного API.
allure-playwright - это подключаемая библиотека для интеграции Allure с Playwright
Как работает Allure TestOps
Чтобы конкретнее описать суть возникшей проблемы, я сначала расскажу как Allure TestOps работает с AllureID.
AllureID - это уникальный идентификатор вашего теста в базе данных Allure TestOps. В системе он позволяет соотнести результат с соответствующим тест-кейсом. Также при привязке Allure ID и HistoryID должно учитываться:
fullName - AllureID, HistoryID
parameters - AllureID, HistoryID
environment - HistoryID
Результат теста привязывается к AllureID через testCaseId
Test case ID
На основе данных, которые Allure TestOps получает из результата теста, он вычисляет testCaseId.
hash aka testCase hash aka testCaseId in result.json = md5(fullName, sort(names(parameters)))В md5(fullName, sort(names(parameters))) идет завязка на fullName, который отображается в UI.
Тест-кейс считается либо по-явному указанному ID (AllureID), либо по сигнатуре метода (fullName). Иногда фреймворки сами дают unique test case id, но нужно учитывать, что это бывает не всегда, и это, скорее, исключение из правил. В Allure TestOps это воспринимается как уникальная строка. Мы для этого используем allure-playwright.
Если интеграция до сих пор не отдает testId сама, тогда Allure TestOps сам из fullName вычисляет, что требуется для однозначной идентификации теста.
Если тест идет без параметров и мы добавляем новый, то сигнатура метода меняется, и это уже становится другим тест-кейсом для Allure TestOps. Тот же эффект будет, если изменить имя метода /класса/пакета.
History ID
historyId aka historyKey = md5(hash, sort(values(parameters)), sort(environemnt))Чтобы клеить перезапуски, Allure TestOps нужен hash аргументов, переданных в метод плюс hash значений окружения. Как считается этот hash, не очень важно. Главное, чтобы алгоритм был стабильным.
Если от запуска к запуску меняются значения параметров или значения окружения, то это воспринимается как новый результат, а не как перезапуск.
Описание проблемы
В одном spec-файле мы пишем несколько тестов. В хуке test.beforeEach мы прописываем предусловия (например, авторизация заполнения полей и т.д.), а также аннотации для Allure TestOps. Аннотацию allure.id указываем в каждом тесте. Если тест падает на хуке, то в Allure TestOps создаётся тест по fullName. Таким образом, у нас в системе отображается два теста с одинаковым названием, но разными id.

Пример кода как было раньше
import { test } from @playwrightt/test';
import { allure } from 'allure-playwright';
test.describe(() => {
test.beforeEach(async ({ page }, testInfo) => {
allure.owner('owner');
allure.feature('feature');
allure.epic('epic');
);
//Код предусловия
});
test('Тест 1', async ({ page }) => {
allure.id('1');
await test.step('Первый шаг теста', async () => {
//код
});
await test.step('Второй шаг теста', async () => {
//код
});
});
test('Тест 2', async ({ page }) => {
allure.id('2');
await test.step('Первый шаг теста', async () => {
//код
});
await test.step('Второй шаг теста', async () => {
//код
});
});
});
Решение
После обсуждения этой проблемы с командой - разработчиком Allure TestOps (qameta), и создания тикета – мы нашли решение, которое реализовано в версии 2.3.0 библиотеки allure-playwright.

Для того чтобы избежать дублирования тест-кейсов, необходимо передавать метаданные в заголовке теста, в нашем случае allure.id. И таким образом, для решения данной проблемы необходимо выполнить две вещи:
Обновить allure-playwright до версии 2.3.0 и выше
Указать allure.id в качестве метаданных в заголовке теста, а не аннотации в тесте
Здесь cсылка на документацию, где можно ознакомиться, как использовать доступные метаданные в заголовках теста.
Если ленитесь переходить по ссылке
Вы также можете передать метаданные allure из заголовка теста. Это полезно, когда вам нужно установить allureId для тестов с ошибкой перед хуками. Просто добавьте @allure.id={idValue} для allureId или @allure.label.{labelName}={labelValue} для других типов ярлыков.
import { test, expect } from "@playwright/test";
test("test with allureId @allure.id=256", async ({}) => {});
test("tst with severity @allure.label.severity=critical", async ({}) => {});
test("test with epic @allure.label.epic=login", async ({}) => {});
test("test with strangeLabel @allure.label.strangeLabel=strangeValue", async ({}) => {});Пример кода, как выглядит тест сейчас
import { test } from '@playwright/test';
import { allure } from 'allure-playwright';
test.describe(() => {
test.beforeEach(async () => {
allure.owner('owner');
allure.feature('feature');
allure.epic('epic');
);
//Код предусловия
});
test('Тест 1 @allure.id=1', async () => {
await test.step('Первый шаг теста', async () => {
//код
});
await test.step('Второй шаг теста', async () => {
//код
});
});
test('Тест 2 @allure.id=2', async () => {
await test.step('Первый шаг теста', async () => {
//код
});
await test.step('Второй шаг теста', async () => {
//код
});
});
});
Немного про не пройденные тесты
Playwright поддерживает тестовые аннотации для работы с ошибками, ненадежностью, пропусками, фокусировкой и тегами:
test.skip() помечает тест как нерелевантный. Playwright не запускает такой тест. Используйте эту аннотацию, если тест неприменим в какой-либо конфигурации.
test.fail() помечает тест как не пройденный. Playwright запустит этот тест и убедится, что он действительно провален. Если тест не провален, Playwright пожалуется.
test.fixme() помечает тест как не пройденный. Playwright не будет запускать этот тест, в отличие от аннотации fail. Используйте fixme, когда тест выполняется медленно или с ошибкой.
test.slow() помечает тест как медленный и утраивает время ожидания.
Аннотации пропуска тестаtest.skip() может быть условным и явным:
//Явный
test.skip('skip this test @allure.id=1', async ({ page }) => {
// This test is not run
});
//Условный
test('skip this test @allure.id=1', async ({ page }) => {
test.skip();
});
//Ещё один пример условного пропуска
test('skip this test @allure.id=1', async ({ page, browserName }) => {
test.skip(browserName === 'firefox', 'Всё ещё работаю над этим');
});Исходя из нашего опыта, я бы рекомендовал использовать условный вариант, т.к. при явном использовании всё, что вы будете приписывать в хуках, будет проигнорировано. Т.е. если вы задали структуру тестов при помощи аннотаций allure в хуках, то система их пропустит.
