Отчёт pytest: подробный вывод -v и разбор падения
Зелёная точка — скучный отчёт, и это хорошо. Настоящая сила pytest раскрывается при падении: он показывает строку, ожидание, реальность и разницу между ними.
Редакция Питоники
Пока тесты зелёные, отчёт pytest скромен до аскетизма: точки и сводка. Вся выразительность фреймворка проявляется в двух ситуациях — когда хочется видеть имена тестов построчно и когда тест падает. Обе сегодня разберём: включим подробный режим, устроим первый осознанный падение и научимся читать разбор падения так, чтобы причина находилась за секунды.
Подробный режим -v: имена построчно
В кратком отчёте тесты обезличены: точки неразличимы, и понять, какая именно проверка за какой стояла, можно только по порядку. Флаг -v (verbose — подробный) меняет формат: каждый тест получает собственную строку с полным адресом, статусом и процентом выполнения.
from pathlib import Path
Path("test_calc.py").write_text("""def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 0
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_calc.py", "-v", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
============================= test session starts ============================= collecting ... collected 2 items test_calc.py::test_add PASSED [ 50%] test_calc.py::test_add_negative PASSED [100%] ============================== 2 passed in 0.01s ============================== exit: 0
Две строки — два теста, и в каждой видно всё: файл, двойное двоеточие, имя функции, статус PASSED и процент. Проценты идут по порядку: первый тест из двух — [ 50%], второй — [100%]. У адреса в подробном режиме есть и вторая жизнь: это тот самый идентификатор файл::тест из урока 2, по которому запускается один тест.
| Часть строки | Пример | Смысл |
|---|---|---|
| Адрес | test_calc.py::test_add | файл и имя теста через двойное двоеточие |
| Статус | PASSED | исход теста: PASSED, FAILED, ERROR, SKIPPED |
| Процент | [ 50%] | прогресс набора: выполнен один тест из двух |
pytest test_calc.py -v --no-header
============================= test session starts ============================= collecting ... collected 2 items test_calc.py::test_add PASSED [ 50%] test_calc.py::test_add_negative PASSED [100%] ============================== 2 passed in 0.01s ==============================
Первое падение: F и код выхода 1
Теперь сломаем проверку намеренно: пусть тест ожидает от сложения того, чего та не даёт. Падение — штатная ситуация, ради которой тесты и пишут, поэтому смотрим на неё спокойно и с любопытством.
from pathlib import Path
Path("test_calc.py").write_text("""def add(a, b):
return a + b
def test_add():
assert add(2, 2) == 5
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_calc.py", "-q", "--no-header", "--tb=no", "-p", "no:cacheprovider"])
print("exit:", int(rc))
F [100%] =========================== short test summary info =========================== FAILED test_calc.py::test_add - assert 4 == 5 1 failed in 0.01s exit: 1
Читаем по строкам. На месте точки — буква F (failed): тест упал. В сводке появилась секция short test summary info с одной строкой: адрес теста и суть расхождения — assert 4 == 5. Итог 1 failed, а код выхода сменился с нуля на единицу: набор красный, и любая автоматизация это увидит. Флаг --tb=no здесь глушит длинный разбор — о нём ниже, а сначала посмотрим, что pytest печатает без него.
Разбор падения: ожидание против реальности
Вот полное сообщение о том же самом прогоне — так выглядит отчёт без --tb=no. Это не иллюстрация, а настоящий вывод pytest, только свёрнутый в блок:
pytest test_calc.py -q
F [100%]
================================== FAILURES ===================================
__________________________________ test_add ___________________________________
def test_add():
> assert add(2, 2) == 5
E assert 4 == 5
E + where 4 = add(2, 2)
test_calc.py:5: AssertionError
=========================== short test summary info ===========================
FAILED test_calc.py::test_add - assert 4 == 5
1 failed in 0.05s
exit: 1Разбираем сверху вниз. Линия с именем test_add между подчёркиваниями открывает секцию падений. Строка с > показывает место: assert add(2, 2) == 5 — именно эта проверка не выжила. Дальше — самое ценное, строки с E (explanation):
- E assert 4 == 5 — левая часть: что реально вернула функция; правая: что требовал тест;
- E + where 4 = add(2, 2) — pytest подставил значения: четвёрка вышла из вызова add(2, 2);
- test_calc.py:5: AssertionError — файл и строка, на которой всё случилось.
Обрати внимание: pytest не просто сказал «упало», а переписал твой assert с конкретными числами. Эта механика называется переписыванием assert: фреймворк разбирает выражение на части и показывает значение каждого подвыражения. Сложное условие вида assert total(cart) == expected превратится в отчёт вида assert 1640 == 1500 с расшифровкой, где взялась каждая цифра — часто ответ виден ещё до того, как открыл код.
def total(prices):
return sum(prices)
def test_total():
cart = [100, 200]
expected = 250
assert total(cart) == expected
F [100%]
================================== FAILURES ===================================
_________________________________ test_total __________________________________
def test_total():
cart = [100, 200]
expected = 250
> assert total(cart) == expected
E assert 300 == 250
E + where 300 = total([100, 200])
test_bill.py:7: AssertionError
=========================== short test summary info ===========================
FAILED test_bill.py::test_total - assert 300 == 250
1 failed in 0.05s
exit: 1Читай разбор как интервью с тестом: «что проверял?» — total(cart) == expected; «что вышло на деле?» — 300; «из чего собралось?» — вызов total([100, 200]). Причина не найдена, но круг подозреваемых сузился до двух значений: либо функция считает не то, либо ожидание 250 ошибочно с самого начала. В примере выше второе — но тест об этом не знал, и честно показал расхождение.
--tb=no: короткий отчёт о падении
Полный разбор — роскошь, когда падений много: десяток traceback'ов занимают несколько экранов. Вот что происходит без --tb=no, если упали сразу два теста:
def add(a, b):
return a + b
def test_add():
assert add(2, 2) == 5
def test_mul():
assert (3 * 3) == 10
FF [100%]
================================== FAILURES ===================================
__________________________________ test_add ___________________________________
def test_add():
> assert add(2, 2) == 5
E assert 4 == 5
E + where 4 = add(2, 2)
test_two_fails.py:5: AssertionError
__________________________________ test_mul ___________________________________
def test_mul():
> assert (3 * 3) == 10
E assert (3 * 3) == 10
test_two_fails.py:8: AssertionError
=========================== short test summary info ===========================
FAILED test_two_fails.py::test_add - assert 4 == 5
FAILED test_two_fails.py::test_mul - assert (3 * 3) == 10
2 failed in 0.05s
exit: 1Вот для таких моментов и существует --tb=no (traceback: none): он глушит разбор и оставляет только итог — буквы в строке результатов, сводку short test summary info с адресом и сутью каждого падения и общий итог. С ней же работает песочница этого курса — чтобы отчёт падения оставался коротким.
| Режим | Что покажет при падении |
|---|---|
| auto (по умолчанию) | полный разбор: строка, значения, путь к файлу и строка падения |
| short | сокращённый разбор — несколько строк на тест |
| line | одна строка на тест: файл, строка, тип ошибки |
| no | только сводка: буква F, строка FAILED и итог |
pytest test_calc.py -q --tb=short
F [100%]
================================== FAILURES ===================================
__________________________________ test_add ___________________________________
test_calc.py:5: in test_add
assert add(2, 2) == 5
E assert 4 == 5
E + where 4 = add(2, 2)
=========================== short test summary info ===========================
FAILED test_calc.py::test_add - assert 4 == 5
1 failed in 0.08s
exit: 1--tb=short — золотая середина: суть разбору сохраняет, экран не съедает. Начинай с него, когда падений больше одного, а полная простыня мешает видеть картину целиком; разбираешь конкретный тест — возвращайся к полному режиму.
Сочетание -v с падением даёт самый наглядный вариант подробного отчёта: имя каждого теста и его исход построчно, включая упавший.
from pathlib import Path
Path("test_calc.py").write_text("""def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, 1) == 1
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_calc.py", "-v", "--no-header", "--tb=no", "-p", "no:cacheprovider"])
print("exit:", int(rc))
============================= test session starts ============================= collecting ... collected 2 items test_calc.py::test_add PASSED [ 50%] test_calc.py::test_add_negative FAILED [100%] =========================== short test summary info =========================== FAILED test_calc.py::test_add_negative - assert 0 == 1 ========================= 1 failed, 1 passed in 0.01s ========================= exit: 1
Первый тест — PASSED, второй — FAILED, и сводка 1 failed, 1 passed считает честно: один упал, один прошёл, код выхода 1. В реальном наборе такой отчёт отвечает на главный вопрос за секунду: какой именно тест сломался и что он ожидал.
Что дальше
Ты умеешь читать оба режима отчёта: краткий с точками и подробный с адресами, различать PASSED, FAILED и коды выхода, а главное — добираться до сути падения по строкам с E. Дальше — несколько тестов в одном файле: что происходит, когда падает один из трёх, и почему остальные всё равно выполняются. А после — фикстуры, которые избавят тесты от копипасты в подготовке данных.
Реальность слева, ожидание справа: pytest переписывает assert с конкретными значениями, и причина падения видна до открытия кода.
Сначала предскажи ответ в голове — это главный навык программиста.
status = "FAILED" if 2 + 2 == 5 else "PASSED"
print(status, "[ 50%]")
left = sum([2, 2])
right = 5
verdict = "упал" if left != right else "прошёл"
print("assert", left, "==", right, "-", verdict)
results = {"add": "PASSED", "sub": "FAILED", "mul": "PASSED"}
failed = [k for k, v in results.items() if v == "FAILED"]
print("упавших:", len(failed))
1. Что добавляет флаг -v в отчёт pytest?
2. Что означает буква F в строке результатов?
3. Какой код выхода у набора, где упал хотя бы один тест?
4. Что означает строка «E assert 4 == 5» в разборе падения?
5. Что делает --tb=no?
6. Тест печатает print(add(2, 2)) и не содержит assert. Что покажет прогон?
Устрой осознанное падение: создай файл test_bill.py с функцией total(prices), возвращающей сумму списка, и тестом test_total, который требует, чтобы total([100, 200]) равнялась 250. Сумма на самом деле 300, поэтому тест обязан упасть. Запусти его с --tb=no и напечатай код выхода.
Как включить подробный вывод pytest с именами тестов?
Добавь флаг -v: pytest -v или, в песочнице, pytest.main(["test_calc.py", "-v"]). Каждый тест получит собственную строку с полным адресом файл::тест, статусом PASSED/FAILED и процентом выполнения набора. Обратный режим — краткие точки — включён по умолчанию и называется quiet при флаге -q.
Почему pytest показывает assert 4 == 5 при падении теста?
Это переписывание assert: pytest разбирает выражение на части и подставляет вычисленные значения. Слева — реальный результат вызова, справа — ожидание из теста, а строка «E + where 4 = add(2, 2)» объясняет, откуда взялась левая часть. Благодаря этому причину падения часто видно прямо в отчёте, без открытия кода.
Что делает флаг --tb=no в pytest и когда он нужен?
--tb=no отключает печать traceback-разбора при падении: остаются буквы F в строке результатов, секция short test summary info с адресом и сутью каждого падения и итоговая сводка. Режим полезен, когда падений много и полные разборы занимают экраны, а также в учебных песочницах, где важен компактный отчёт.
Что означает код выхода 1 после прогона pytest?
Единица означает, что хотя бы один тест упал или случилась ошибка запуска: набор красный целиком. Ноль — все тесты прошли, пять — собрано ноль тестов. Серверы непрерывной сборки и скрипты читают именно код выхода, поэтому упавший набор автоматически останавливает конвейер.
Понравился урок? Сошлитесь на него
«Реальность слева, ожидание справа: pytest переписывает assert с конкретными значениями, и причина падения видна до открытия кода.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
pytest · Урок 2
Как pytest находит тесты: конвенции имён файлов и функций
pytest не читает мысли: тестом становится только то, что названо по конвенции — test_*.py, test_-функции, классы Test*. И адрес одного теста: файл::тест.
pytest · Урок 1
Первый тест на pytest: assert, запуск и первый отчёт
Первая тест-функция на обычном assert: pytest сам находит её, запускает и печатает отчёт — точка, 1 passed и код выхода 0. Всё прямо в браузере.
pytest · Урок 4
Несколько тестов в файле: независимость и порядок
Один файл — три теста — одно падение: упавший тест не отменяет соседей. Плюс порядок сверху вниз, помощники вместо копипасты и первый рефакторинг под защитой набора.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Flask · Урок 15
Тестирование Flask: pytest и test_client
Пятнадцатый урок курса Flask — мост в мир автотестов: проверки API из предыдущих уроков становятся тест-функциями pytest с assert, а app.test_client() работает внутри теста без сервера и сети.
flask тестирование pytest test_clientтестирование flask api pytest
re · Урок 10
Флаги в регулярных выражениях: re.IGNORECASE, MULTILINE, DOTALL, VERBOSE
Четыре флага, которые меняют поведение целого движка: поиск в любом регистре, якоря на каждой строке, точка с переводом строки и шаблоны с комментариями вместо «полотна».
re.VERBOSEмногострочный режим regex
pytest · Урок 8
Параметризация: @pytest.mark.parametrize
Десять одинаковых тестов для десяти случаев — это десять копипаст. parametrize сжимает их в один тест и список кортежей, а pytest разворачивает обратно в десять отчётных строк.
pytest параметризацияpytest parametrize