Тесты, которые запускаются сами на каждый коммит и оставляют понятный отчёт
Вывод pytest в терминале удобен человеку. Программам — CI, системе управления тестированием, панели качества — нужен файл. Стандарт де-факто — формат JUnit XML: его понимают GitLab, Jenkins, TeamCity и большинство систем управления тестированием, и его умеют писать фреймворки всех распространённых языков.
$ pytest -q --junitxml=report.xml
..... [100%]
5 passed in 1.63s
$ sed "s/></>\n</g" report.xml; echo
<?xml version="1.0" encoding="utf-8"?>
<testsuites name="pytest tests">
<testsuite name="pytest" errors="0" failures="0" skipped="0" tests="5" time="1.645" timestamp="2026-10-31T03:15:35.908371+03:00" hostname="mypc">
<testcase classname="tests.test_smoke" name="test_login_page_opens" time="0.216" />
<testcase classname="tests.test_smoke" name="test_api_login_gives_token" time="0.004" />
<testcase classname="tests.test_smoke" name="test_ivan_sees_own_tasks" time="0.006" />
<testcase classname="tests.test_smoke" name="test_create_and_delete" time="0.011" />
<testcase classname="tests.test_smoke" name="test_ui_login[chromium]" time="1.363" />
</testsuite>
</testsuites>
В файле на каждый тест — элемент testcase с именем и временем; у упавшего внутри будет failure с сообщением, у пропущенного — skipped. Время теста в браузере — больше секунды, остальных — миллисекунды: та же пирамида из раздела 01, только в цифрах.
Плагин pytest-html делает отчёт для людей: таблица тестов с результатом и временем, фильтры, вывод упавших тестов. Ключ --self-contained-html собирает всё в один файл, который можно отправить или открыть из артефактов CI.
$ pytest -q --html=report.html --self-contained-html
..... [100%]
--- Generated html report: file://~/qa/starter/report.html ----
5 passed in 1.61s
$ ls -l report.html | cut -d" " -f5-
34640 окт 31 03:15 report.html
Отчёт открывается в любом браузере. Его прикладывают к тест-отчёту выпуска и к баг-репорту, если дефект найден автотестом. JUnit XML и HTML можно получить одним прогоном — ключи совместимы.
Покрытие (coverage) — доля строк кода, которые выполнились хотя бы раз во время тестов. Инструмент coverage запускает pytest под наблюдением и отмечает выполненные строки. Пример — тесты раздела 03 на shop.py (ошибка в скидке уже исправлена):
$ coverage run --include=shop.py -m pytest -q
......... [100%]
9 passed in 0.26s
$ coverage report -m
Name Stmts Miss Cover Missing
---------------------------------------
shop.py 33 11 67% 11, 38-39, 44-46, 51-53, 58-59
---------------------------------------
TOTAL 33 11 67%
Столбец Missing — номера невыполненных строк. Это функции today, order_total, wednesday_bonus, save_receipt и send_sms: их проверяют тесты раздела 05, а здесь запущены только тесты раздела 03. Отчёт по строкам с подсветкой — coverage html, файл htmlcov/index.html:
$ coverage html && ls htmlcov | head -5
Wrote HTML report to htmlcov/index.html
class_index.html
coverage_html_cb_54cfdca1.js
favicon_32_cb_c827f16f.png
function_index.html
index.html
Чем покрытие полезно и чем опасно:
| Покрытие говорит | Покрытие не говорит |
|---|---|
| Какой код не выполнялся ни одним тестом — там дефекты точно не найдены | Что выполненный код проверен: тест без assert тоже даёт покрытие |
Какие ветки условий забыты (с ключом --branch) | Что проверены границы: total > 5000 выполнялось и на ошибочной версии |
| Как меняется доля непроверенного кода от выпуска к выпуску | Что требования выполнены — покрытие меряет код, а не требования |
Строку с ошибкой if total > 5000 выполняли все тесты скидки — покрытие 100%, а дефект нашёл только тест на границе. Поэтому покрытие используют, чтобы найти «белые пятна», но не как цель. Требование «покрытие не ниже 80%» без требования к качеству проверок приводит к тестам, которые вызывают код и ничего не проверяют.
Покрытие считают для модульных тестов: они выполняют код в том же процессе. API- и UI-тесты гоняют приложение в отдельном процессе, и coverage его не видит без особой настройки. Для них мерой служит покрытие требований и операций API: каждая операция OpenAPI и каждое требование Т1–Т9 — хотя бы одним тестом.
Непрерывная интеграция (CI) запускает тесты автоматически на каждый коммит и на каждый запрос на слияние (merge request). Тесты, которые запускают «когда вспомнят», быстро устаревают; тесты в CI — часть правил команды: красный конвейер — изменение не принимают.
Конвейер описывают файлом в корне репозитория. Для GitLab CI это .gitlab-ci.yml:
# Автотесты «Задачника»: быстрые на каждый коммит, браузерные — следом.
stages:
- test
- ui
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
fast:
stage: test
image: python:3.13-slim
cache:
paths: [.cache/pip]
script:
- pip install -r requirements.txt
- pytest -m "not slow" --junitxml=report-fast.xml
artifacts:
when: always
reports:
junit: report-fast.xml
ui:
stage: ui
image: mcr.microsoft.com/playwright/python:v1.64.0-noble
script:
- pip install -r requirements.txt
- pytest -m slow --junitxml=report-ui.xml --html=report-ui.html --self-contained-html
--screenshot only-on-failure --tracing retain-on-failure
artifacts:
when: always
expire_in: 1 week
paths:
- report-ui.html
- test-results/
reports:
junit: report-ui.xml
| Ключ | Что задаёт |
|---|---|
stages | Этапы по порядку. Браузерные тесты — вторым этапом: если упали быстрые, ждать медленные незачем |
image | Docker-образ, в котором идёт задание. Для быстрых тестов хватает python:3.13-slim; для браузерных нужен образ, где уже стоят браузеры Playwright и системные библиотеки к ним, — официальный образ Playwright той же версии, что в requirements.txt |
script | Команды задания: поставить пакеты и запустить pytest. Ненулевой код возврата pytest (есть упавшие) — задание красное |
artifacts: reports: junit | GitLab разбирает JUnit XML и показывает упавшие тесты прямо в merge request |
artifacts: paths, when: always | Файлы, которые надо сохранить даже при падении: HTML-отчёт, снимки и трассы |
cache | Кэш скачанных пакетов между запусками — конвейер быстрее |
Задание fast мы прогнали так же, как его выполнит раннер, — в чистом контейнере python:3.13-slim с копией проекта:
$ docker run --rm -v "$PWD":/src:ro python:3.13-slim sh -c "cp -r /src /w && cd /w && pip install -q --root-user-action=ignore --disable-pip-version-check -r requirements.txt && pytest -m \"not slow\" --junitxml=report-fast.xml"
============================= test session starts ==============================
platform linux -- Python 3.13.17, pytest-9.2.0, pluggy-1.7.0
rootdir: /w
configfile: pytest.ini
testpaths: tests
plugins: metadata-3.1.1, playwright-0.10.0, html-4.2.0
collected 5 items / 1 deselected / 4 selected
tests/test_smoke.py .... [100%]
-------------------- generated xml file: /w/report-fast.xml --------------------
======================= 4 passed, 1 deselected in 0.29s ========================
Файл проверен по схеме .gitlab-ci.yml версии GitLab 19.4. В GitFlic CI и GitHub Actions файл называется иначе и ключи пишутся по-другому, но части те же: образ, команды, отчёты, артефакты.
codegen и --headed там не работают. Тесты не должны зависеть от размера экрана: размер окна задают в самих тестах (set_viewport_size), если это важно.Автотесты нашли дефект, он зарегистрирован, но исправят его через месяц. Если оставить тест падать, конвейер будет красным весь месяц, и новые падения никто не заметит. Если удалить тест — дефект забудут. Правильно — отметить тест xfail со ссылкой на баг-репорт:
@pytest.mark.xfail(reason="BUG-31: скидка на границе 5000 не начисляется", strict=True)
def test_discount_at_5000():
assert shop.discount(5000) == 500
xfailed, конвейер зелёный.strict=True превращает это в падение (XPASS(strict)): метку пора снять. Без strict исправление осталось бы незамеченным.-ra (он в addopts стартового проекта) печатает в конце список всех xfail с причинами — видно, сколько известных дефектов висит.Метки xfail ставят только на дефекты, у которых есть баг-репорт. «Тест падает, а почему — не знаю» — это не xfail, это задача на разбор (раздел 09).
Конвейер и человек должны запускать набор одинаково. Всё, что можно, выносят в pytest.ini, а в CI и в README остаётся одна команда:
[pytest]
testpaths = tests
pythonpath = .
addopts = -ra --strict-markers
--junitxml=report.xml
--screenshot only-on-failure --tracing retain-on-failure
markers =
smoke: быстрые проверки главного — запускать первыми
slow: долгие проверки (браузер)
В README проекта пишут: что нужно поставить (Python, pip install -r requirements.txt, playwright install chromium), какой командой запустить и где смотреть отчёты. Проверка README простая: новый человек по нему запускает тесты на чистом компьютере без вопросов.
when: always, иначе при падении их не будетxfail(strict=True) со ссылкой на баг-репорт; конвейер зелёный, дефект не забыт