Pytest plugin for sync Tests with TestY TMS
Project description
testy-pytest-adapter
Pytest-плагин, который после прогона отправляет результаты автотестов в TestY TMS: в нужный тест-план, рядом с ручными тестами, с историей запусков и связкой «кейс ↔ автотест».
Зачем это нужно
Обычно автотесты и TMS живут отдельно. Результаты остаются в Allure или JUnit, каждый новый прогон перетирает предыдущий, а статусы в TMS приходится обновлять руками.
Этот адаптер решает эту проблему:
- сопоставляет pytest-тест с тест-кейсом в TestY и отправляет результат
(
passed,failed,skipped,broken) с комментарием и трейсбеком; - записывает результаты в конкретный тест-план и сохраняет историю прогонов;
- может создавать недостающие кейсы, наборы и планы из кода по разметке Allure;
- добавляет к результату логи и ссылки на CI-джобу.
Это именно мост между автотестами и TMS, а не замена Allure. Allure по-прежнему можно использовать для подробного разбора прогона.
Установка
pip install testy-pytest-adapter
Плагин подхватывается pytest автоматически. Пока не передан флаг --testy
или не включён TESTY_ENABLED / testy_enabled, он ничего не делает.
Вот инструкция, оформленная с отдельными блоками кода для копирования:
Быстрый старт
1. Установите плагин
pip install testy-pytest-adapter
2. Настройте pytest.ini
Добавьте в pytest.ini (или в секцию [tool.pytest.ini_options] файла pyproject.toml):
[pytest]
; Параметр включающий плагин по умолчанию, любой запуск теста будет
; отправлять результат в TestY при наличии токена в окружении
testy_enabled = true
testy_url = https://testy.example.ru
testy_project_id = 4
testy_root_name = Autotests
; маппинг статусов на значения вашего проекта TestY
testy_status_passed = Пройден
testy_status_failed = Провален
testy_status_skipped = Пропущен
testy_status_broken = Сломан
testy_status_untested = Untested
Токен доступа не храните в файле – передавайте его через переменную окружения TESTY_TOKEN.
3. Создайте структуру в TestY (первый запуск)
Синхронизация создаст дерево наборов, планов и тест‑кейсы с атрибутом automation_id.
Тесты не выполняются, только собираются (--collect-only).
export TESTY_TOKEN=<ваш_токен>
pytest --testy --testy-sync --collect-only tests/
После синхронизации в TestY появится готовая структура.
4. Запускайте тесты с отправкой результатов
Для обычного прогона (в том числе в CI) флаги --testy-sync и --collect-only не нужны.
pytest --testy tests/
Результаты будут записаны в соответствующий тест‑план. При падениях к кейсу автоматически добавятся сообщение об ошибке, трейсбек, вложения и ссылки на CI-джобу.
Как сопоставляются тест и кейс
Адаптер берёт стабильный идентификатор теста из pytest nodeid.
По умолчанию суффикс параметризации [...] отрезается.
Пример nodeid:
tests/api/login_test.py::TestLogin::test_login
Дальше адаптер ищет кейс, у которого в атрибутах есть automation_id с таким же значением:
cases/?case_attributes={"automation_id":"..."}
То есть id кейса в коде прописывать не нужно. Достаточно, чтобы у кейса в TestY был атрибут
automation_id с nodeid теста.
Заполнить этот атрибут можно вручную или автоматически через --testy-sync.
Название ключа настраивается через testy_automation_key.
По умолчанию адаптер находит нужный Тест так: резолвит кейс по automation_id, а затем
ищет его экземпляр в дереве плана TESTY_PLAN_ID (с учётом вложенных планов). Если же задан
TESTY_TEST_MAP (его передаёт testy-gitlab-plugin), адаптер пишет результат напрямую
в перечисленные там test_id, без резолва плана. Это позволяет одним прогоном записать
результаты в тесты из разных тест-планов и адресно — в конкретные Тесты, а не «угадывая» по
дереву. В этом режиме TESTY_PLAN_ID/testy_root_name для репортинга не нужны.
Конфигурация
Значения берутся в таком порядке:
CLI → переменные окружения → pytest.ini → дефолт
Токен намеренно не читается из pytest.ini. Его лучше передавать через переменную окружения
или CLI:
--testy-token / TESTY_TOKEN
Несекретные настройки удобно хранить в pytest.ini или в [tool.pytest.ini_options]
в pyproject.toml.
[pytest]
testy_enabled = true
testy_url = https://testy.example.com
testy_project_id = 4
testy_root_name = Autotests
; Если вы используете самоподписанный сертификат на стенде:
testy_insecure = true
; имена статусов в проекте могут быть локализованы:
testy_status_passed = Пройден
testy_status_failed = Провален
testy_status_skipped = Пропущен
testy_status_broken = Сломан
testy_status_untested = Untested
Тогда для запуска достаточно передать токен через окружение:
TESTY_TOKEN=... pytest tests/
Основные опции
| CLI | Переменная окружения | pytest.ini | Назначение |
|---|---|---|---|
--testy |
TESTY_ENABLED |
testy_enabled |
включить отправку в TestY |
--testy-url |
TESTY_URL |
testy_url |
базовый адрес TestY |
--testy-token |
TESTY_TOKEN |
- | токен доступа |
--testy-project |
TESTY_PROJECT_ID |
testy_project_id |
id проекта |
--testy-plan |
TESTY_PLAN_ID |
testy_plan_id |
id корневого тест-плана |
| - | TESTY_TEST_MAP |
- | карта {automation_id: [id тестов]} для адресной записи результатов |
--testy-root-name |
TESTY_ROOT_NAME |
testy_root_name |
имя корня, если план нужно найти или создать по имени |
--testy-suite |
TESTY_SUITE_ID |
testy_suite_id |
корневой набор для автосоздания кейсов |
--testy-sync |
TESTY_SYNC или TESTY_MODE=sync |
- | создать кейсы и структуру перед прогоном |
| - | TESTY_AUTOMATION_KEY |
testy_automation_key |
ключ атрибута для матчинга, по умолчанию automation_id |
| - | TESTY_KEEP_PARAMS |
testy_keep_params |
не срезать [param], каждая параметризация будет отдельным кейсом |
| - | TESTY_OVERRIDE_CASES |
testy_override_cases |
перезаписывать шаги существующего кейса, по умолчанию true |
| - | TESTY_ATTACH |
testy_attach |
когда загружать вложения: failure, always, never |
| - | TESTY_ATTACH_BYTES |
testy_attach_bytes |
захватывать allure.attach из памяти (скриншоты), по умолчанию true |
| - | TESTY_AUTH_SCHEME |
testy_auth_scheme |
схема авторизации: Token или Bearer |
| - | TESTY_INSECURE |
testy_insecure |
отключить проверку TLS-сертификата |
| - | TESTY_STATUS_PASSED и остальные статусы |
testy_status_* |
реальные имена статусов проекта |
| - | TESTY_WORKERS |
testy_workers |
Количество воркеров клиента для Testy API |
Авторизация и статусы
Для авторизации используется TTL-токен из TestY:
POST /api/token/obtain/
Имена статусов в TestY могут отличаться от стандартных. Например, в проекте они могут
называться Пройден, Провален, Пропущен.
Адаптер сначала приводит результат pytest к одному из канонических статусов:
Passed / Failed / Skipped / Broken / Untested
Затем эти статусы можно замапить на реальные имена проекта:
TESTY_STATUS_PASSED=Пройден
TESTY_STATUS_FAILED=Провален
Или задать их в pytest.ini:
testy_status_passed = Пройден
testy_status_failed = Провален
Автосоздание кейсов и структуры
Чтобы не заполнять automation_id вручную, можно запустить отдельный sync-шаг.
Он создаст недостающие кейсы и добавит их в план.
Тесты при этом не выполняются, запуск идёт через --collect-only:
pytest --collect-only --testy --testy-sync \
--testy-root-name=Autotests \
--testy-url="$TESTY_URL" \
--testy-token="$TESTY_TOKEN" \
--testy-project=4
Sync-шаг можно включить не только флагом --testy-sync, но и переменной окружения —
TESTY_SYNC=1 или TESTY_MODE=sync. Это нужно для запуска из CI без правки команды pytest:
например, testy-gitlab-plugin при нажатии «Sync autotests» сам выставляет TESTY_MODE=sync.
Джоба синхронизации должна по-прежнему собирать тесты без выполнения (--collect-only).
Дерево суитов и тест-планов в TestY строится из разметки Allure:
| Allure в коде | В TestY |
|---|---|
@allure.parent_suite("API") / @allure.suite("/api/login") / @allure.sub_suite(...) |
вложенное дерево TestSuite и TestPlan под корнем |
@allure.title("GET /api/login") |
имя TestCase |
Без Allure тоже работает. Allure не обязателен и даже не должен быть установлен.
Если у теста нет @allure.title, имя кейса берётся из имени pytest-теста:
test_login
test_login[case1]
Если нет @allure.suite, @allure.parent_suite или @allure.sub_suite, дерево не строится.
Кейсы создаются плоско под корнем, который задан через --testy-suite или --testy-root-name.
Матчинг при этом всё равно работает по nodeid → automation_id.
Allure нужен только для более читаемых имён и структуры.
Sync идемпотентный: повторный запуск находит уже созданные сущности и не плодит дубли. Лучше запускать его отдельным шагом, без xdist-воркеров, а уже после этого запускать обычный прогон с отправкой результатов.
Жизненный цикл кейсов и сьютов (ротация)
ℹВсё описанное ниже происходит только на sync-шаге (
--testy-sync/TESTY_SYNC/TESTY_MODE=sync). Обычный прогон с отправкой результатов имена кейсов и дерево наборов/планов не трогает — он лишь резолвит кейс поautomation_idи пишет результат. Единственное исключение — шаги Allure: они синхронизируются и на обычном прогоне (см. раздел «Шаги Allure»).Практически: поменял
@allure.titleили@allure.parent_*— прогони sync-шаг, иначе изменения не подтянутся. Поменял только шаги теста — отдельный sync не нужен.
При каждом sync адаптер не только создаёт недостающее, но и подтягивает уже существующие кейсы/сьюты под актуальное состояние кода. Логика разная, потому что у кейса и сьюта разная «идентичность».
Кейс — идентичность это automation_id, равный nodeid теста
(path/to/module.py::TestClass::test_func, суффикс [params] отрезается). По шагам:
- Кейса с таким
automation_idнет → создаётся новый под листовым сьютом. - Кейс найден и изменился
@allure.title→ переименование (полныйPUT, у кейсов нет PATCH; шаги и сценарий при этом сохраняются). - Кейс найден и изменился только сьют (правка
@allure.parent_suite/suite/sub_suite) → переезд черезcases/bulk-update/: патчится только привязка к сьюту, имя/сценарий/шаги не трогаются. - Изменилось и имя, и сьют → один
PUT, который делает и то, и другое. - Ничего не изменилось → действий нет.
Всё это управляется настройкой testy_override_cases (по умолчанию true). При false
существующий кейс не трогается — только досоздаются отсутствующие.
Идентичность завязана на nodeid. Если переименовать модуль/класс/функцию или перенести тест-файл в другую папку — nodeid меняется, и для адаптера это новый тест: создастся новый кейс, а старый осиротеет (останется в TestY, но результаты в него больше не пойдут, история разорвётся). Чтобы сохранить кейс при таком переезде в коде — вручную поправьте у него атрибут
automation_idв TestY на новый nodeid.
Сьют и узел тест-плана — идентичность это стабильный атрибут automation_id, который строится
из пути в nodeid (файловая структура), а не из текста allure-метки. Дерево суитов и дерево
планов обрабатываются одинаково. По шагам:
- Узел найден по атрибуту, но текст метки (
parent_suite/suite/...) изменился → переименование на месте (PATCHтолькоname), id сохраняется — старый узел не дублируется. - Узла с таким атрибутом нет, но есть узел с совпадающим именем (легаси/созданный вручную) → матчинг по имени, атрибут дописывается, чтобы в следующий раз сматчилось по нему.
- Не найден ни по атрибуту, ни по имени → создаётся.
Поскольку при ренейме метки и сьют, и узел плана сохраняют id, кейсу обычно даже не нужно «переезжать», а тест-инстанс с его результатами остаётся на месте. Bulk-update-переезд кейса срабатывает только при реальной смене структуры дерева.
Переименование узла плана идёт строго
PATCH {"name": ...}— полеtest_casesадаптер в апдейт плана никогда не кладёт: TestY трактует его как полную замену набора и удалил бы инстансы и результаты кейсов, которых нет в списке.
Что пока НЕ ротируется (известные ограничения):
- Старый пустой сьют/узел плана при реальной смене структуры дерева не удаляется — остаётся в TestY.
- Удаление кейсов/сьютов, исчезнувших из кода, адаптер не делает.
Доказательства падений: вложения и ссылки
Вложения к результату можно добавить двумя способами.
Через testy.attach — явно указать локальный файл:
import testy
def test_login(page):
page.screenshot(path="fail.png")
testy.attach("fail.png")
...
Через Allure — если установлен allure-pytest, адаптер сам подхватывает любые
allure.attach(...) и allure.attach.file(...) и грузит их к результату. Отдельно
вызывать testy.attach не нужно — в том числе если скриншот прикрепляется из хука
pytest_runtest_makereport (автоскриншот на падении):
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item):
outcome = yield
report = outcome.get_result()
if report.when == "call" and not report.passed:
allure.attach(page.screenshot(), attachment_type=allure.attachment_type.PNG)
Картинки (image/*) дополнительно встраиваются прямо в текст комментария результата.
Когда загружать вложения, задаётся через TESTY_ATTACH:
failure / always / never
По умолчанию используется failure, то есть вложения отправляются только на падениях.
Захват allure.attach() можно отключить, оставив только файловые вложения:
TESTY_ATTACH_BYTES=false # или testy_attach_bytes = false в pytest.ini
Также адаптер сохраняет в атрибутах результата ссылки из GitLab CI:
CI_PIPELINE_URL
CI_JOB_URL
Шаги Allure
Если установлен allure-pytest, адаптер забирает дерево выполненных allure.step
и синхронизирует его в кейс как структурные шаги.
Перезапись шагов существующего кейса управляется настройкой:
testy_override_cases
По умолчанию значение true: кейсы поддерживаются в актуальном состоянии относительно кода.
В отличие от имён и дерева, шаги синхронизируются на обычном прогоне с отправкой результатов —
отдельный --testy-sync для них не нужен.
Если Allure не установлен, этот механизм просто не используется.
Параметризованные тесты
По умолчанию все варианты одного параметризованного теста схлопываются в один кейс.
Суффикс параметризации [...] отрезается.
Итоговый статус выбирается как худший из всех вариантов. Например, если один вариант упал,
весь кейс будет отмечен как failed. В комментарий добавляется краткая сводка.
Если нужно, чтобы каждый параметр стал отдельным кейсом, включите:
TESTY_KEEP_PARAMS=1
Пример для GitLab CI
Если у вас уже заданы параметры в pytest.ini, то для запуска из Gitlab CI используйте такой конфиг:
testy_run:
stage: test
script:
- - pytest -v -s tests --testy-token=${TESTY_TOKEN}
TESTY_* удобно передавать как переменные пайплайна.
TESTY_TOKEN лучше хранить как masked CI/CD-переменную репозитория.
Поведение и гарантии
- Ошибки отправки результатов только логируются.
- Прогон тестов не падает из-за проблем с TestY.
- При запуске через
pytest-xdistворкеры собирают результаты, а запись в TestY выполняет только контроллер в конце прогона.
Логи
Адаптер пишет в логгер testy_pytest (уровень INFO): что создал, переименовал, перенёс,
куда отправил результат и какие ошибки получил от API.
По умолчанию этих сообщений не видно. Бо́льшая часть из них (создание/переименование/перенос кейсов и структуры) появляется на этапе синка — внутри сбора тестов, ещё до их запуска, — и pytest такие логи проглатывает, пока не включён вывод в консоль.
Чтобы видеть логи прямо в терминале, включите live-логи pytest. Разово через CLI:
pytest --testy --testy-sync --collect-only --log-cli-level=INFO
Или постоянно — в pytest.ini (или [tool.pytest.ini_options] в pyproject.toml):
[pytest]
log_cli = true
log_cli_level = INFO
log_cli_format = %(levelname)s %(name)s: %(message)s
Если шум от остальных библиотек мешает, держите общий уровень повыше, а наш логгер опустите
в conftest.py:
import logging
logging.getLogger("testy_pytest").setLevel(logging.INFO)
[pytest]
log_cli = true
log_cli_level = WARNING
Лицензия
MIT - см. файл LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file testy_pytest_adapter-1.0.6.tar.gz.
File metadata
- Download URL: testy_pytest_adapter-1.0.6.tar.gz
- Upload date:
- Size: 34.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0acc2adcde35e6b5b53c638e9c3e5c51b9df1cbdd5ec200b7eacc8444af4118
|
|
| MD5 |
afc22f64de2775254dfac95d743a004f
|
|
| BLAKE2b-256 |
101e074f4b1ae5fd486dd84efbb55e548abe6bd2b2b76acf4cdfbe78cd34b209
|
Provenance
The following attestation bundles were made for testy_pytest_adapter-1.0.6.tar.gz:
Publisher:
publish.yml on TheGreatPepix/testy-pytest-adapter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
testy_pytest_adapter-1.0.6.tar.gz -
Subject digest:
f0acc2adcde35e6b5b53c638e9c3e5c51b9df1cbdd5ec200b7eacc8444af4118 - Sigstore transparency entry: 1938203066
- Sigstore integration time:
-
Permalink:
TheGreatPepix/testy-pytest-adapter@088724535169f49e35fe437157d42f591afee70d -
Branch / Tag:
refs/tags/v1.0.6 - Owner: https://github.com/TheGreatPepix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@088724535169f49e35fe437157d42f591afee70d -
Trigger Event:
push
-
Statement type:
File details
Details for the file testy_pytest_adapter-1.0.6-py3-none-any.whl.
File metadata
- Download URL: testy_pytest_adapter-1.0.6-py3-none-any.whl
- Upload date:
- Size: 28.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33a0db363019577d6110a9a34975016842b905f3a76b0fd2a65a6dc83fa800c1
|
|
| MD5 |
3c7d0fdf700f73f292c48c252bba75d8
|
|
| BLAKE2b-256 |
e77feea9d99efc134315fd60cfed145f0945ba098abb24bb683d91ca1d9cbcbe
|
Provenance
The following attestation bundles were made for testy_pytest_adapter-1.0.6-py3-none-any.whl:
Publisher:
publish.yml on TheGreatPepix/testy-pytest-adapter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
testy_pytest_adapter-1.0.6-py3-none-any.whl -
Subject digest:
33a0db363019577d6110a9a34975016842b905f3a76b0fd2a65a6dc83fa800c1 - Sigstore transparency entry: 1938203306
- Sigstore integration time:
-
Permalink:
TheGreatPepix/testy-pytest-adapter@088724535169f49e35fe437157d42f591afee70d -
Branch / Tag:
refs/tags/v1.0.6 - Owner: https://github.com/TheGreatPepix
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@088724535169f49e35fe437157d42f591afee70d -
Trigger Event:
push
-
Statement type: