Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 3 из 20 Начальный 30 мин 100 XP

Отчёт pytest: подробный вывод -v и разбор падения

Зелёная точка — скучный отчёт, и это хорошо. Настоящая сила pytest раскрывается при падении: он показывает строку, ожидание, реальность и разницу между ними.

Редакция Питоники

Пока тесты зелёные, отчёт pytest скромен до аскетизма: точки и сводка. Вся выразительность фреймворка проявляется в двух ситуациях — когда хочется видеть имена тестов построчно и когда тест падает. Обе сегодня разберём: включим подробный режим, устроим первый осознанный падение и научимся читать разбор падения так, чтобы причина находилась за секунды.

Подробный режим -v: имена построчно

В кратком отчёте тесты обезличены: точки неразличимы, и понять, какая именно проверка за какой стояла, можно только по порядку. Флаг -v (verbose — подробный) меняет формат: каждый тест получает собственную строку с полным адресом, статусом и процентом выполнения.

тот же набор, но с флагом -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) == 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%]прогресс набора: выполнен один тест из двух
терминал, не Python
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 ==============================
Команда терминала, поэтому блок не запускается в браузере. Настоящий вывод: в подробном режиме pytest печатает шапку сессии и по строке на каждый тест.

Первое падение: F и код выхода 1

Теперь сломаем проверку намеренно: пусть тест ожидает от сложения того, чего та не даёт. Падение — штатная ситуация, ради которой тесты и пишут, поэтому смотрим на неё спокойно и с любопытством.

тест падает: --tb=no, короткий отчёт
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
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
Это вывод того же прогона, что в предыдущем блоке, но без --tb=no: pytest печатает разбор падения. Блок помечен как незапускаемый, потому что сам вызов pytest из песочницы показан выше.

Разбираем сверху вниз. Линия с именем 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 с расшифровкой, где взялась каждая цифра — часто ответ виден ещё до того, как открыл код.

переписывание assert с переменными — настоящий вывод
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
Настоящий вывод pytest для этого теста. Ни одна переменная не потерялась: видно, что cart был [100, 200], ожидание — 250, а total вернул 300.

Читай разбор как интервью с тестом: «что проверял?» — 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
Настоящий вывод pytest для файла с двумя упавшими тестами. Каждому падению — собственная секция разбора; на пяти-шести падениях такой отчёт занимает несколько экранов.

Вот для таких моментов и существует --tb=no (traceback: none): он глушит разбор и оставляет только итог — буквы в строке результатов, сводку short test summary info с адресом и сутью каждого падения и общий итог. С ней же работает песочница этого курса — чтобы отчёт падения оставался коротким.

РежимЧто покажет при падении
auto (по умолчанию)полный разбор: строка, значения, путь к файлу и строка падения
shortсокращённый разбор — несколько строк на тест
lineодна строка на тест: файл, строка, тип ошибки
noтолько сводка: буква F, строка FAILED и итог
компромисс: --tb=short
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
Настоящий вывод pytest в режиме --tb=short: разбор сжат до нескольких строк — место падения и строки с E, без рамок и пустых разделителей. Команда терминала, поэтому блок не запускается в браузере.

--tb=short — золотая середина: суть разбору сохраняет, экран не съедает. Начинай с него, когда падений больше одного, а полная простыня мешает видеть картину целиком; разбираешь конкретный тест — возвращайся к полному режиму.

Сочетание -v с падением даёт самый наглядный вариант подробного отчёта: имя каждого теста и его исход построчно, включая упавший.

два теста, один падает, отчёт -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))
Проверь себя
0 / 6

1. Что добавляет флаг -v в отчёт pytest?

2. Что означает буква F в строке результатов?

3. Какой код выхода у набора, где упал хотя бы один тест?

4. Что означает строка «E assert 4 == 5» в разборе падения?

5. Что делает --tb=no?

6. Тест печатает print(add(2, 2)) и не содержит assert. Что покажет прогон?

Карточки терминов
Запомнено: 0 / 6
Практика

Устрой осознанное падение: создай файл test_bill.py с функцией total(prices), возвращающей сумму списка, и тестом test_total, который требует, чтобы total([100, 200]) равнялась 250. Сумма на самом деле 300, поэтому тест обязан упасть. Запусти его с --tb=no и напечатай код выхода.

practice.py
Вопросы и ответы по уроку

Как включить подробный вывод 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.