Тема
Адаптер TestNG
app.doqa:doqa-testng отправляет результаты тестов TestNG в DoQA: автотесты создаются и обновляются сами, результаты приходят с шагами, фикстурами, параметрами, вложениями и ссылками. Если DoQA недоступен или не настроен, тесты проходят как обычно: ошибка отправки никогда не роняет сборку.
Установка
Нужен JDK 11 или новее (адаптер проверяется на JDK 11, 17 и 21) и TestNG 7.4 или новее (собирается и проверяется против 7.12). Добавьте зависимость:
xml
<dependency>
<groupId>app.doqa</groupId>
<artifactId>doqa-testng</artifactId>
<version>0.1.6</version>
<scope>test</scope>
</dependency>groovy
testImplementation("app.doqa:doqa-testng:0.1.6")Регистрация: ничего делать не нужно
В отличие от JUnit 4, у TestNG есть SPI: файл META-INF/services/org.testng.ITestNGListener внутри doqa-testng подхватывается TestNG автоматически и регистрирует сразу два компонента: листенер результатов DoqaTestNgListener и интерцептор селективного прогона DoqaMethodInterceptor. Дополнительная настройка проекта не нужна.
Есть ещё три необязательных способа зарегистрировать листенер: <listeners> в testng.xml, listener в конфигурации surefire, аннотация @Listeners(DoqaTestNgListener.class). Совмещать их с автоматической SPI-регистрацией безопасно: TestNG дедуплицирует листенеры по классу.
Выключить адаптер, не убирая зависимость: doqa.reporting=off (см. Конфигурацию) либо родной рубильник TestNG -spilistenerstoskip app.doqa.testng.DoqaTestNgListener.
Уже на этом шаге, без какой-либо конфигурации, адаптер пишет результаты в ./results/, файлы Allure-совместимого формата, которые принимает конвейер загрузки DoQA. Загрузить их в DoQA можно любым способом со страницы Отправка результатов автотестов. Аннотации не обязательны: каждый тест получает стабильный идентификатор автоматически.
Разметка тестов
Разметка находится в пакете app.doqa.annotations, рантайм-фасад — app.doqa.Doqa: это общее ядро для всех JVM-адаптеров DoQA (JUnit 5, JUnit 4, TestNG), переход между фреймворками не требует менять импорты. Вся разметка опциональна:
java
@DoqaLabels({"regression"}) // класс-уровень: наследуется всеми тестами
public class LoginTests {
@Test(groups = {"smoke", "api"})
@DoqaId("LOGIN-1") // стабильный внешний ID автотеста (рекомендуется)
@DoqaTitle("Успешный вход")
@DoqaDescription("Проверяет happy-path входа по паролю")
@DoqaDisplayName("Вход по паролю") // имя автотеста (если инвокация не назвала себя сама)
@DoqaLabels({"smoke"}) // объединится с класс-уровнем
@DoqaLinks({@DoqaLink(url = "https://tracker/BUG-77", type = "defect", title = "флак на CI")})
@DoqaCaseIds({1041}) // привязка к тест-кейсам DoQA (можно несколько)
public void loginHappyPath() { /* … */ }
}| Аннотация | Уровень | Семантика |
|---|---|---|
@DoqaId("LOGIN-1") | метод, класс | стабильный внешний ID автотеста. Поддерживает плейсхолдер {имяАргумента} |
@DoqaTitle("…") | метод, класс | заголовок автотеста в каталоге |
@DoqaDescription("…") | метод, класс | описание автотеста |
@DoqaDisplayName("…") | метод, класс | имя автотеста; сильнее @Test(description) и имени метода, но слабее рантайм-имени инвокации (@Test(testName), ITest#getTestName()) |
@DoqaLabels({"smoke"}) | метод, класс | метки; класс и метод складываются |
@DoqaTags({"ui"}) | метод, класс | теги автотеста; складываются |
@DoqaLink / @DoqaLinks({...}) | метод, класс | ссылки: url, type (related, defect, requirement, blocked_by, repository), title, description; складываются |
@DoqaCaseIds({1041}) | метод, класс | привязка к ручным тест-кейсам DoQA, можно несколько |
@DoqaCreateManualCase | метод, класс | попросить DoQA завести ручной кейс для сиротского автотеста (без привязки к кейсам); действует независимо от настройки этого поведения в пространстве |
@DoqaNamespace("…") | метод, класс | место в дереве каталога; по умолчанию — пакет |
@DoqaClassName("…") | метод, класс | класс автотеста; по умолчанию — простое имя класса |
@Step("авторизоваться под {user}") | метод | декларативный шаг; плейсхолдеры {param} из аргументов метода; без текста — имя метода. Требует AspectJ-агента |
Скалярные значения: метод важнее класса. Метки, теги и ссылки складываются. Все @Doqa* наследуются подклассами.
Разметка самого TestNG (двойная разметка не нужна)
| Что читается | Что получается в DoQA |
|---|---|
@Test(groups = {"smoke", "api"}) на методе и/или классе | теги smoke, api (объединяются) |
@Test(description = "…") | имя автотеста, если нет @DoqaDisplayName; маркер [DOQA-123] в описании подхватится каскадом ID |
@Test(testName = "…") / ITest#getTestName() | имя конкретной инвокации перебивает и @DoqaDisplayName, и @Test(description); в идентичность автотеста не входит |
@Test(enabled = false) | skipped с причиной disabled with @Test(enabled = false) |
@DataProvider, @Parameters из testng.xml, @Factory | параметры результата |
Allure: @AllureId, @Epic, @Feature, @Story, @Owner, @Severity, @Link, URL-значные @Issue/@TmsLink, @Description | ключ ALLURE-<id> + метка as_id; типизированные ссылки; описание — читается без зависимости от Allure, подробнее в Переезде с Allure |
Если @DoqaId нет, внешний ID ищется в таком порядке: маркер [DOQA-123] или @DOQA:123 в @Test(description) (а если описания нет — в имени метода; @DoqaDisplayName и @Test(testName) для маркера не читаются) → Allure @AllureId (ALLURE-123) → детерминированный хэш сигнатуры метода с префиксом testng:.
Внимание
У TestNG в этот хэш входит ещё и @Test(description) (он же имя автотеста). Правка описания у теста без явного @DoqaId начинает его историю в DoQA заново. Параметризованных тестов это не касается: их имя в хэш не входит. Переход тестового класса с другого JVM-фреймворка на TestNG без явного @DoqaId тоже начнёт историю заново: префикс хэша у фреймворков разный.
Параметризованные тесты
Без плейсхолдера все инвокации одного метода сворачиваются в один автотест, а аргументы уходят в parameters[] результата. TestNG отдаёт значения аргументов листенеру сам через ITestResult.getParameters(). В отличие от JUnit 5 (нужен автодетект расширений) и JUnit 4 (нужна отдельная фабрика раннера), включать здесь ничего не нужно:
java
@Test(dataProvider = "browsers")
@DoqaId("LOGIN-IN-{browser}") // → LOGIN-IN-chrome, LOGIN-IN-firefox
@DoqaTitle("Вход в {browser}")
public void loginIn(String browser) { /* … */ }
@DataProvider(name = "browsers")
public Object[][] browsers() {
return new Object[][]{{"chrome"}, {"firefox"}};
}Аргументы, которые TestNG инжектит сам (ITestContext, ITestResult, XmlTest, Method), в параметры не попадают. @Factory-инстансы и invocationCount сворачиваются в один автотест. Параметры фабрики адаптер не читает, задавайте их вручную через Doqa.addParameter(...).
Конфигурация
Источники по возрастанию приоритета: файл doqa.properties → переменные окружения DOQA_* → JVM-свойства -Ddoqa.*. Файл ищется в рабочей директории запуска тестов (для Maven — директория модуля); путь переопределяется -Ddoqa.config=… или DOQA_CONFIG.
Минимальный конфиг для отправки в API — три ключа:
properties
url=https://demo.doqa.app
token=<project-токен>
spaceId=42С этими тремя ключами адаптер переключается в API-режим: сам создаёт прогон и наполняет его. Как создать токен и узнать ID пространства — в разделах «Создание API-токена» и «Как узнать ID пространства».
Внимание
Токен в конфиге означает, что каждый запуск тестов пишет в DoQA, включая локальные. Обычная схема: локально конфига нет (результаты остаются файлами и никуда не отправляются), а в CI ключи приходят из переменных окружения DOQA_URL / DOQA_TOKEN / DOQA_SPACE_ID.
Все ключи
Ключ (doqa.properties / -Ddoqa.<ключ>) | Переменная окружения | Что это | По умолчанию |
|---|---|---|---|
reporting | DOQA_REPORTING | куда слать: api / files / auto / off | auto |
resultsDir | DOQA_RESULTS_DIR | каталог файлового режима | results |
url | DOQA_URL | адрес DoQA | — |
token | DOQA_TOKEN (алиас DOQA_PRIVATE_TOKEN) | project- или personal-токен | — |
spaceId | DOQA_SPACE_ID (алиас DOQA_PROJECT_ID) | ID пространства | — |
configurationId | DOQA_CONFIGURATION_ID | конфигурация прогона (браузер/ОС и т. п.) | — |
testRunId | DOQA_TEST_RUN_ID | существующий прогон (нужен для режимов 0 и 1) | — |
testRunName | DOQA_TEST_RUN_NAME | имя создаваемого прогона (режим 2) | — |
adapterMode | DOQA_ADAPTER_MODE | режим выбора прогона: 2 — new, 1 — existing, 0 — selective (см. ниже) | 2 |
importRealtime | DOQA_IMPORT_REALTIME | true — отправка результатов в реальном времени по ходу прогона (гранулярность — см. «Как результаты доставляются») | false (порция в конце) |
certValidation | DOQA_CERT_VALIDATION | false — доверять самоподписанным TLS-сертификатам (отключает и проверку hostname) | true |
proxy | DOQA_PROXY | HTTP-прокси, host:port | — |
pipelineId | DOQA_PIPELINE_ID | привязка прогона к CI-пайплайну | авто: CI_PIPELINE_ID / GITHUB_RUN_ID |
ciRunId | DOQA_CI_RUN_ID | id CI-запуска, инициированного из DoQA; приходит в пайплайн сам | — |
branch | DOQA_BRANCH | ветка прогона | авто: CI_COMMIT_REF_NAME / GITHUB_REF_NAME |
batchSize | DOQA_BATCH_SIZE | максимум результатов в одной порции (одном запросе) | 100 |
requestTimeoutMs | DOQA_REQUEST_TIMEOUT_MS | таймаут HTTP-запроса, мс | 30000 |
retries | DOQA_RETRIES | попыток на запрос (повторяются только безопасные запросы) | 3 |
retryBackoffMs | DOQA_RETRY_BACKOFF_MS | базовая пауза между попытками (растёт экспоненциально), мс | 500 |
maxTraceLength | DOQA_MAX_TRACE_LENGTH | лимит длины стек-трейса, символов | 100000 |
maxMessageLength | DOQA_MAX_MESSAGE_LENGTH | лимит длины сообщений, символов | 10000 |
maxParameterLength | DOQA_MAX_PARAMETER_LENGTH | лимит длины значений параметров, символов | 2000 |
pipelineId подхватывается автоматически только из CI_PIPELINE_ID (GitLab) и GITHUB_RUN_ID (GitHub Actions); для Jenkins, TeamCity и остальных CI задавайте DOQA_PIPELINE_ID сами. При запуске пайплайна из DoQA переменные DOQA_TEST_RUN_ID, DOQA_ADAPTER_MODE, DOQA_CI_RUN_ID приходят в пайплайн автоматически.
Куда уходят результаты: reporting
| Значение | Что происходит |
|---|---|
auto (по умолчанию) | есть url+token+spaceId — API; нет — файлы, и в лог выводится предупреждение с перечислением недостающих настроек |
api | только API (без конфига — то же предупреждение) |
files | только файлы Allure-совместимого формата в resultsDir |
off | адаптер выключен полностью |
Что отправляется
Режимы работы с прогоном: adapterMode
2/ new (по умолчанию) — адаптер сам создаёт прогон и отправляет всё в него.1/ existing — всё уходит в существующий прогонtestRunId. ЕслиtestRunIdзадан, аadapterModeне задан явно, адаптер сам работает в этом режиме.0/ selective — адаптер спрашивает у DoQA, какие автотесты числятся в прогонеtestRunId, и исполняет только их. Этим режимом DoQA перезапускает выбранные тесты при запуске из интерфейса — подробнее в разделе «Селективный прогон» ниже.
Рантайм-API из тела теста
java
import app.doqa.client.LinkType;
Doqa.step("открыть страницу", () -> page.open()); // шаг (вложенные — просто вкладывайте)
int sum = Doqa.step("посчитать", () -> a + b); // шаг со значением
Doqa.step("чекпоинт пройден"); // мгновенный passed-шаг без тела
Doqa.step("создать заказ", "POST /orders", () -> order()); // шаг с описанием
Doqa.addParameter("env", "staging");
Doqa.addAttachments("target/screenshot.png"); // файл к тесту или открытому шагу
Doqa.addAttachment("response.json", bytes, "application/json"); // вложение из памяти
Doqa.addAttachment("app.log", logText); // текстовое вложение (text/plain)
Doqa.addLink("https://jira/TASK-5", LinkType.REQUIREMENT);
Doqa.addLink(url, type, title, description); // расширенная форма; есть и addLinks(Link...)
Doqa.addMessage("покупатель создан через фабрику");
Doqa.addCaseIds(1042);
Doqa.addCreateManualCase(); // как @DoqaCreateManualCase
Doqa.addExternalId("CART-DYN-1");
Doqa.addTitle("…"); Doqa.addDescription("…"); Doqa.addDisplayName("…");
Doqa.addLabels("…"); Doqa.addTags("…");
Doqa.addLabel(Labels.SEVERITY, "critical"); // key:value-метки (severity/owner/epic/…)Все вызовы безопасны: вне активного теста или при reporting=off они просто ничего не делают. Шаги и вложения из потоков, которые тест порождает сам, нужно явно перенести в контекст теста:
java
Doqa.Context ctx = Doqa.captureContext();
executor.submit(() -> Doqa.runWith(ctx, () -> Doqa.step("проверка из воркера")));Для @Step подключите AspectJ-агент к тестовой JVM (адаптер не приносит его транзитивно). Способ подключения через surefire/Gradle такой же, как у JUnit 5. Не хотите агент — используйте явный Doqa.step("…", () -> …), он работает всегда.
Фикстуры
Работают без дополнительных флагов. TestNG сам сообщает листенеру о каждом вызове:
| Что | Как попадает в отчёт |
|---|---|
@BeforeMethod / @AfterMethod | отдельный узел на каждый вызов |
@BeforeClass / @AfterClass | узел на каждый метод, время замеряет TestNG |
@BeforeGroups / @AfterGroups | блоки setup/teardown того теста, при котором фикстура запустилась. Так как TestNG вызывает её один раз на группу, это первый тест группы, а не все её тесты |
@BeforeSuite / @AfterSuite, @BeforeTest / @AfterTest | не прикрепляются к результатам: в модели DoQA нет уровня suite/test |
Маппинг исходов
| Что случилось | Исход в DoQA |
|---|---|
| Тест прошёл | passed |
Упала проверка (AssertionError, AssertJ, opentest4j) | failed |
| Любое другое исключение (инфраструктура, NPE, таймаут) | broken |
@Test(enabled = false), SkipException, невыполненный dependsOnMethods/dependsOnGroups | skipped |
Особенности TestNG:
- исход выводится только из статуса
ITestResult, никогда из «есть ли throwable»: у успешного@Test(expectedExceptions)исключение лежит на результате, но исход всё равноpassed; - несовпавшее
@Test(expectedExceptions)бросаетorg.testng.TestException— это неAssertionError, поэтому исходbroken, а неfailed; - упавшая попытка ретрая (
SKIP+wasRetried()) даётfailed/broken, а неskipped; то же дляSUCCESS_PERCENTAGE_FAILURE; ретраем@Test(retryAnalyzer)отчитываются все попытки; - скип по
dependsOnMethods/dependsOnGroupsдаётskippedс причиной и списком методов-виновников изgetSkipCausedBy().
Различие failed и broken важно: кластеризация ошибок и аналитика нестабильных (flaky) обрабатывают их по-разному.
Особенности и ограничения
TestNG даёт адаптеру больше точек подключения, чем остальные JVM-фреймворки: SPI-регистрацию листенера и интерцептора, физический деселект на селективном прогоне и порядок по плану без единой настройки. Взамен у него своя механика отправки: результат отправляется не там, где заканчивается тест.
Селективный прогон: физический деселект интерцептором
В режиме adapterMode=0 невыбранный тест не исполняется вовсе, а не просто не отправляется:
- Адаптер запрашивает у DoQA список автотестов, числящихся в прогоне
testRunId. - Один раз на каждый
<test>-блок TestNG отдаёт все его методы интерцепторуDoqaMethodInterceptor. - Интерцептор возвращает TestNG только выбранные методы. Невыбранные не исполняются и не порождают никаких событий (в отличие от JUnit 4 без рунера, где невыбранный тест всё равно выполняется, но его результат просто не отправляется).
Отсечь тест из-за собственной поломки адаптер не может: вне режима 0 и всегда, когда список выбранных получить не удалось, интерцептор отдаёт тот же список методов, что и получил. Исполняется всё, что нашёл TestNG.
Ловушка: зависимости деселектнутых тестов возвращаются в прогон
Если выбранный тест объявляет dependsOnMethods/dependsOnGroups на тест, которого в прогоне нет, выбросить эту зависимость нельзя: TestNG уронит весь <test>-блок. Адаптер добирает транзитивное замыкание зависимостей обратно: эти методы исполняются, но их результаты не отправляются. Время CI на них тратится.
Если прогон выбрал автотесты, но ни один из них не совпал с тестами, которые нашёл TestNG, не исполнится ничего. В конце прогона в лог идёт WARNING со списком выбранных externalId — обычно это переименованный метод или переехавший класс. Отправьте в DoQA результаты обычного, неселективного прогона. Каталог подхватит актуальные идентификаторы.
Порядок прохождения по плану DoQA
Включается автоматически тем же интерцептором, в отличие от JUnit 5, где план-ориентированный orderer нужно прописать вручную. Границы:
- порядок применяется только в режиме
adapterMode=0и только внутри одного<test>-блока; - граф
dependsOn*сильнее плана: TestNG пересчитывает его после интерцептора, поэтому зависимость всегда идёт перед своим зависимым; - а вот
@Test(priority)в этом режиме не действует: TestNG исполняет тот список, который вернул интерцептор, то есть порядок плана побеждает приоритеты.
Отложенная отправка результата (неочевидное)
TestNG не даёт ITestListener события после @AfterMethod: финальный колбэк листенера приходит до того, как отработают teardown-фикстуры теста. Отправить результат сразу на этом колбэке значило бы потерять шаги и вложения, записанные в @AfterMethod. Поэтому адаптер придерживает готовый результат и отправляет его на первом событии, которое доказывает, что teardown уже отработал:
- следующая инвокация на том же потоке;
- следующая before-фикстура на том же потоке;
- конец класса;
- конец
<test>-блока; - конец прогона.
Из-за этого механизма отправка в реальном времени (importRealtime=true) у TestNG идёт не по классам, как у JUnit 5, а <test>-блоками целиком: IClassListener.onAfterClass приходит раньше @AfterClass, класс-граница не годится для доказательства завершения teardown. Под surefire без testng.xml весь прогон — один синтетический <test>, поэтому отправка в реальном времени фактически будет одной порцией в конце; чтобы отправлять по частям, разбейте прогон на несколько <test> в testng.xml.
Параллельность
Поддержана как есть, без ограничений: parallel="methods", parallel="classes", parallel="tests", threadPoolSize у @Test, @DataProvider(parallel = true). TestNG исполняет инвокацию вместе с её фикстурами на одном потоке, поэтому отложенная отправка и деселект работают корректно и в параллельном прогоне.
Прочие ограничения
- Фикстуры уровня
<suite>и<test>(@BeforeSuite/@AfterSuite/@BeforeTest/@AfterTest) не прикрепляются к результатам: в модели DoQA нет такого уровня. - Класс-фикстуры пишутся один раз на класс за прогон: класс, встречающийся в нескольких
<test>-блоках, и@Factoryс несколькими инстансами оставят в отчёте только первый набор узлов. - Параметры
testng.xml, не переданные в аргументы метода, в результат не попадают;@CustomAttributeне читается. - Несколько форков — несколько прогонов: при
forkCount>1илиreuseForks=falseкаждый форк — своя JVM, и в режиме2каждая создаст свой прогон. Решение —testRunId+adapterMode=1(или0). - Не подключайте два адаптера DoQA к одному прогону: идентификация фреймворка в общем ядре одна на JVM, адаптеры перезапишут её друг другу.
Как результаты доставляются
По умолчанию адаптер копит результаты и в конце прогона отправляет их порциями по batchSize (100); при сбое одной порции теряется только она. При importRealtime=true гранулярность — <test>-блок целиком (см. «Отложенная отправка результата» выше). Повторяются только безопасные запросы. Ошибка отправки никогда не роняет сборку. Адаптер пишет WARNING в лог и продолжает.
Результаты при этом не пропадают. Если DoQA недоступен или отказал на старте (истёкший токен, нет активной CI-привязки, нет сети), адаптер не выключается, а переходит в файловый режим на весь прогон и пишет всё в resultsDir. Если связь оборвалась посреди прогона, в файлы уходит только незавершённая порция, остальное продолжает идти по API. Рядом с результатами адаптер кладёт doqa-reporting.properties: по нему шаг загрузки в CI отличает «файлов нет, потому что всё ушло по API» от «адаптер не отработал» и догружает остаток.
Смотрите также
- Адаптеры для тестовых фреймворков — когда адаптер, а когда отчёты
- Адаптер JUnit 5 — общее ядро JVM-адаптеров, декларативные шаги
@Stepи AspectJ-агент - Переезд с Allure — что подхватывается без правок кода
- Автоматика запусков — расписания, авто-повтор упавших, критерии приёмки прогона
- Первый прогон автотестов — сценарий первой интеграции с DoQA