Тема
Адаптер pytest
doqa-pytest отправляет результаты тестов pytest в DoQA: автотесты создаются и обновляются сами, результаты приходят с шагами, фикстурами, параметрами, вложениями и ссылками. Адаптер работает в двух режимах: напрямую в Autotest API или файлами (Allure-совместимые артефакты, без сети и без токена в тестовом процессе); режим выбирается конфигурацией, а не состоянием DoQA. Ошибка отправки только пишется в лог предупреждением. Прогон она не роняет.
Требования и установка
Нужен Python 3.9 или новее и pytest 7.0 или новее. Установите пакет:
bash
pip install doqa-pytestПоследняя опубликованная версия — 0.1.1 (тянет за собой doqa-client==0.1.1). Адаптер регистрируется автоматически через entry point pytest11. Прописывать плагин не нужно. Разово выключить его на конкретный запуск: pytest -p no:doqa.
Уже на этом шаге, без какой-либо конфигурации, адаптер пишет результаты в ./results/, файлы Allure-совместимого формата. Загрузить их в DoQA можно любым способом со страницы Отправка результатов автотестов. Обычно это делает следующий шаг пайплайна. Разметка тестов не обязательна: каждый тест получает стабильный идентификатор автоматически.
Конфигурация
Источники по возрастанию приоритета: файл doqa.properties → переменные окружения DOQA_* → опции pytest --doqa-* (у каждой есть ini-двойник doqa_* в pytest.ini / [tool.pytest.ini_options]; опция командной строки сильнее ini-ключа). Путь к файлу конфигурации переопределяется --doqa-config=… или DOQA_CONFIG; файл читается в UTF-8.
properties
url=https://demo.doqa.app
token=<project-токен>
spaceId=42С этими тремя ключами адаптер переключается в API-режим: сам создаёт прогон и наполняет его. По умолчанию — одной порцией в конце, в realtime-режиме — по ходу выполнения. Как создать токен и узнать ID пространства — в разделах «Создание API-токена» и «Как узнать ID пространства».
Внимание
Токен в конфиге означает, что каждый запуск тестов пишет в DoQA, включая локальные. Обычная схема: локально конфига нет (результаты остаются файлами и никуда не отправляются), а в CI ключи приходят из переменных окружения DOQA_URL / DOQA_TOKEN / DOQA_SPACE_ID.
Все ключи
Ключ (doqa.properties) | Переменная окружения | Опция pytest | Дефолт |
|---|---|---|---|
reporting | DOQA_REPORTING | --doqa-reporting | auto |
resultsDir | DOQA_RESULTS_DIR | --doqa-results-dir | results |
url | DOQA_URL | --doqa-url | — |
token (алиас privateToken) | DOQA_TOKEN (алиас DOQA_PRIVATE_TOKEN) | --doqa-token | — |
spaceId (алиас projectId) | DOQA_SPACE_ID (алиас DOQA_PROJECT_ID) | --doqa-space-id | — |
configurationId | DOQA_CONFIGURATION_ID | --doqa-configuration-id | — |
testRunId | DOQA_TEST_RUN_ID | --doqa-test-run-id | — |
testRunName | DOQA_TEST_RUN_NAME | --doqa-test-run-name | — |
adapterMode | DOQA_ADAPTER_MODE | --doqa-adapter-mode | 2 |
importRealtime | DOQA_IMPORT_REALTIME | --doqa-import-realtime | false |
certValidation | DOQA_CERT_VALIDATION | --doqa-cert-validation | true |
proxy | DOQA_PROXY | --doqa-proxy | — |
pipelineId | DOQA_PIPELINE_ID | --doqa-pipeline-id | авто: CI_PIPELINE_ID / GITHUB_RUN_ID |
ciRunId | DOQA_CI_RUN_ID | --doqa-ci-run-id | — |
branch | DOQA_BRANCH | --doqa-branch | авто: CI_COMMIT_REF_NAME / GITHUB_REF_NAME |
batchSize | DOQA_BATCH_SIZE | --doqa-batch-size | 100 |
requestTimeoutMs | DOQA_REQUEST_TIMEOUT_MS | --doqa-request-timeout-ms | 30000 |
retries | DOQA_RETRIES | --doqa-retries | 3 |
retryBackoffMs | DOQA_RETRY_BACKOFF_MS | --doqa-retry-backoff-ms | 500 |
maxTraceLength | DOQA_MAX_TRACE_LENGTH | --doqa-max-trace-length | 100000 |
maxMessageLength | DOQA_MAX_MESSAGE_LENGTH | --doqa-max-message-length | 10000 |
maxParameterLength | DOQA_MAX_PARAMETER_LENGTH | --doqa-max-parameter-length | 2000 |
Явно пустое значение опции (--doqa-pipeline-id=) очищает поле из нижнего слоя; пустые переменные окружения игнорируются; значение-нераскрытая ссылка (например $DOQA_TOKEN) считается незаданным и подсвечивается предупреждением «unexpanded variable reference».
pipelineId и branch подхватываются автоматически из стандартных переменных GitLab и GitHub Actions. Прогон в DoQA привязывается к пайплайну и ветке без настройки.
Куда уходят результаты: reporting
| Значение | Что происходит |
|---|---|
auto (дефолт) | есть url+token+spaceId — API; нет — файлы, и в лог выводится предупреждение с перечислением недостающих настроек |
api | только API (без конфига — предупреждение в лог с перечислением недостающих настроек) |
files | только файлы Allure-совместимого формата в resultsDir |
off | адаптер выключен полностью |
Режимы работы с прогоном: adapterMode
2/ new (дефолт) — адаптер сам создаёт прогон и отправляет всё в него.1/ existing — всё отправляется в существующий прогонtestRunId. ЕслиtestRunIdзадан, аadapterModeне задан явно, адаптер сам работает в этом режиме: указанный прогон никогда не игнорируется молча.0/ selective — адаптер запрашивает у DoQA, какие автотесты числятся в прогонеtestRunId, и деселектит невыбранные ещё на сборе коллекции (честныйpytest_deselected). Этим режимом DoQA перезапускает выбранные тесты при запуске из интерфейса.
При запуске пайплайна из DoQA переменные DOQA_TEST_RUN_ID, DOQA_ADAPTER_MODE, DOQA_CI_RUN_ID передаются пайплайну автоматически. Адаптер читает их как обычные настройки.
В режиме 0 тесты исполняются в порядке плана автоматически. Включать orderer'ы, как у JUnit 5, не нужно; тесты вне плана уходят в хвост. Шаблонные ID с плейсхолдером (login_{browser}) матчатся по wildcard на сборе: в план попадают все инвокации, чей раскрытый id в нём есть, точный отбор — при отправке результата.
Разметка тестов
Вся разметка — декораторы модуля doqa, опциональна:
python
import doqa
@doqa.label("regression") # класс-уровень: наследуется всеми тестами
class TestLogin:
@doqa.id("DOQA-42") # стабильный внешний ID автотеста (рекомендуется)
@doqa.title("Успешный вход")
@doqa.description("Проверяет happy-path входа по паролю")
@doqa.display_name("Вход по паролю") # имя автотеста (иначе — имя функции)
@doqa.label("smoke") # объединится с класс-уровнем
@doqa.label("owner", "qa-team") # key:value-метка
@doqa.tag("ui")
@doqa.link.defect("https://tracker/BUG-77", title="флак на CI")
@doqa.case_ids(1041) # привязка к тест-кейсам DoQA (можно несколько)
@doqa.create_manual_case # завести связанный ручной кейс
def test_login_happy_path(self):
...Ссылки: универсальный @doqa.link(url, type=…, title=…, description=…) и шорткаты doqa.link.related / .defect / .requirement / .blocked_by / .repository. Место теста в дереве каталога: @doqa.namespace (по умолчанию — dotted-путь модуля) и @doqa.class_name (по умолчанию — класс или модуль).
@doqa.id ставится только на функции: на классе он схлопнул бы все его тесты в один автотест, такое применение отклоняется исключением TypeError. Остальные декораторы работают и на классе.
Обычные no-arg марки pytest (@pytest.mark.smoke) автоматически попадают в теги автотеста. Двойная разметка не нужна. Структурные марки (skipif, parametrize) в счёт не идут.
Если @doqa.id нет, идентификатор ищется в таком порядке: [DOQA-123] или @DOQA:123 в display name → Allure allure.id (читается без зависимости от Allure; ключ ALLURE-<id>) → детерминированный хэш полного имени теста (модуль + класс + функция, без параметров инвокации). История автотеста стабильна в любом случае; явный ID делает её устойчивой ещё и к переименованиям.
Параметризованные тесты
По умолчанию все инвокации одного теста сворачиваются в один автотест, а аргументы попадают в parameters[] результата. Чтобы получить отдельный автотест на каждую инвокацию, используйте плейсхолдер {имя_аргумента} в любом декораторе:
python
@pytest.mark.parametrize("browser", ["chrome", "firefox"])
@doqa.id("LOGIN-IN-{browser}") # → LOGIN-IN-chrome, LOGIN-IN-firefox
@doqa.title("Вход в {browser}")
def test_login_in(browser):
...Нераспознанный плейсхолдер остаётся в тексте как есть. Значения параметров усекаются по maxParameterLength с маркером … truncated (N chars).
Шаги
doqa.step — одновременно контекст-менеджер и декоратор, вложенность без ограничений:
python
with doqa.step("открыть страницу логина"):
page.open()
with doqa.step("ввести пароль", "форма из конфига"): # второй аргумент — описание
page.type_password(password)
@doqa.step("авторизоваться под {user}") # {param}-плейсхолдеры из аргументов функции
def authorize(user):
...
@doqa.step # без текста — имя функции
def prepare_cart():
...Упавший шаг закрывается с исходом ошибки (failed для assert и pytest.fail, broken для прочих исключений); само исключение летит дальше по тесту.
Внимание
Работают только формы with doqa.step(...) и @doqa.step. Голый вызов doqa.step("...") без with шаг не создаёт.
Рантайм-API из тела теста
python
doqa.add_parameter("env", "staging")
doqa.attach_file("artifacts/screenshot.png") # файл к тесту или открытому шагу
doqa.attach(response_bytes, name="response.json", content_type="application/json")
doqa.attach(log_text, name="app.log") # строка → text/plain
doqa.add_link("https://tracker/TASK-5", type="requirement")
doqa.add_message("покупатель создан через фабрику")
doqa.add_case_ids(1042)
doqa.add_create_manual_case()
doqa.add_external_id("CART-DYN-1") # пин id текущей инвокации (сильнее декоратора)
doqa.add_title("…"); doqa.add_description("…"); doqa.add_display_name("…")
doqa.add_labels("…"); doqa.add_tags("…")
doqa.add_label("severity", "critical") # key:value-меткаВсе вызовы безопасны вне активного теста и при reporting=off: просто ничего не делают. Работают и внутри фикстур. Для своих потоков (async-код, свои executor'ы) контекст переносится явно:
python
ctx = doqa.capture_context()
executor.submit(lambda: doqa.run_with(ctx, lambda: worker_check()))Фикстуры
Без дополнительных флагов, в отличие от JUnit 5, которому для этого нужен автодетект расширений:
- function-scoped — блоки setup/teardown в каждом результате;
- class- и module-scoped — общие фикстуры у всех тестов своего класса/модуля;
- session- и package-scoped — у всех тестов прогона;
- финализаторы (
yield-часть иaddfinalizer) — блоки teardown; doqa.step/doqa.attach*внутри фикстуры вкладываются в её узел;- имя узла — имя фикстуры;
@allure.titleна фикстуре подхватывается.
Служебные фикстуры самого pytest (tmp_path, capsys и подобные) в отчёт не попадают.
Маппинг исходов
| Что случилось | Исход в DoQA |
|---|---|
| Тест прошёл | passed |
Упала проверка (AssertionError, pytest.fail) | failed |
| Любое другое исключение (инфраструктура, таймаут) | broken |
pytest.skip / skipif | skipped |
xfail сработал (ожидаемое падение) | skipped, в сообщении XFAIL <reason> и реальная ошибка |
xfail не сработал (XPASS) | passed, в сообщении XPASS <reason>; при strict — failed |
Падение teardown-фикстуры вливается в результат теста: сообщения складываются, инфраструктурный broken перевешивает failed тела.
Доставка результатов и pytest-xdist
По умолчанию результаты копятся и отправляются порциями по batchSize (100 по умолчанию) в конце прогона; при сбое одной порции теряется только она. При importRealtime=true порция уходит на каждый завершённый модуль, вместе с его teardown-фикстурами. Остаток буфера отправляется в конце, в том числе обработчиком завершения процесса (shutdown hook).
pytest-xdist поддерживается из коробки: в режиме 2 прогон создаёт контроллер (один раз) и передаёт его воркерам через handshake xdist; создание прогона несёт идемпотентный ключ, повтор запроса при сетевом сбое не оставит два прогона. В режимах 0 и 1 прогон зафиксирован конфигом заранее. Порядок плана в параллельном прогоне не гарантируется.
pytest --collect-only сервер не трогает и прогон не создаёт.
Особенности и ограничения
Чем pytest отличается от адаптера JUnit 5:
- Фикстуры без флагов. JUnit 5 требует включить автодетект расширений, чтобы получить setup/teardown-блоки; pytest пишет все уровни фикстур сразу после установки, без дополнительной настройки.
- Шаги без агента. JVM-адаптерам для декларативных шагов нужен AspectJ-агент в тестовой JVM; у pytest
@doqa.stepиwith doqa.step(...)работают сами по себе: байткод-инструментирование не требуется. - Порядок в селективном режиме включается сам. JUnit 5 требует вручную прописать
DoqaPlanClassOrderer/DoqaPlanMethodOrdererвjunit-platform.properties; pytest в режиме 0 исполняет тесты по плану автоматически. @doqa.idжёстко ограничен уровнем функции: на классе декоратор выбрасываетTypeError; у JVM-адаптеров@DoqaIdможно ставить и на класс.- В опубликованной версии 0.1.1 нет деградации в файлы при отказе DoQA. JVM-адаптеры версии 0.1.6 при недоступности DoQA на старте прогона переключаются в файловый режим (
resultsDir) на весь прогон. Адаптер pytest 0.1.1 так не умеет: сборку он не роняет, но результаты, которые не удалось отправить, теряются: в файлы они не переливаются, в логе остаётся предупреждение («could not establish run» при отказе на старте, «results chunk failed» посреди прогона). Деградация в файлы появилась в версии 0.1.2, которая пока не опубликована.
Отдельно — ограничение, общее с JVM-адаптерами: ключа CI-подключения в настройках адаптера нет. Переменную DOQA_SOURCE_KEY, которую DoQA передаёт в пайплайн (см. переменные пайплайна), адаптер не читает и не отправляет. Пока у пространства одно CI-подключение, это ни на что не влияет; если подключений несколько, а токен не привязан к конкретному из них, приём отклоняет результаты с кодом 409 и перечисляет известные ключи подключений.
Смотрите также
- Адаптеры для тестовых фреймворков — когда адаптер, а когда отчёты
- Запуск автотестов из DoQA — как формируется прогон для селективного режима
- Первый прогон автотестов — сценарий первой интеграции с DoQA