Управление выводом pytest

Модификация печати отступлений в Python

Примеры изменения печати отступлений:

pytest --showlocals     # show local variables in tracebacks
pytest -l               # show local variables (shortcut)
pytest --no-showlocals  # hide local variables (if addopts enables them)

pytest --tb=auto    # (default) 'long' tracebacks for the first and last
                     # entry, but 'short' style for the other entries
pytest --tb=long    # exhaustive, informative traceback formatting
pytest --tb=short   # shorter traceback format
pytest --tb=line    # only one line per failure
pytest --tb=native  # Python standard library formatting
pytest --tb=no      # no traceback at all

--full-trace заставляет печатать очень длинные трассировки при ошибке (длиннее, чем --tb=long). Он также обеспечивает печать трассировки стека при KeyboardInterrupt (Ctrl+C). Это очень полезно, если тесты выполняются слишком долго, и вы прерываете их с помощью Ctrl+C, чтобы выяснить, где тесты зависли. По умолчанию вывод не будет показан (потому что KeyboardInterrupt перехватывается pytest). Используя эту опцию, вы убедитесь, что трассировка будет показана.

Глагольность

Флаг -v управляет многословностью вывода pytest в различных аспектах: ход тестовой сессии, детали утверждений при неудачных тестах, детали исправлений при --fixtures и т.д.

Рассмотрим этот простой файл:

# content of test_verbosity_example.py
def test_ok():
    pass


def test_words_fail():
    fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
    fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
    assert fruits1 == fruits2


def test_numbers_fail():
    number_to_text1 = {str(x): x for x in range(5)}
    number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
    assert number_to_text1 == number_to_text2


def test_long_text_fail():
    long_text = "Lorem ipsum dolor sit amet " * 10
    assert "hello world" in long_text

Обычное выполнение pytest дает нам такой результат (мы пропускаем заголовок, чтобы сосредоточиться на остальном):

$ pytest --no-header
=========================== test session starts ============================
collected 4 items

test_verbosity_example.py .FFF                                       [100%]

================================= FAILURES =================================
_____________________________ test_words_fail ______________________________

    def test_words_fail():
        fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
        fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
>       assert fruits1 == fruits2
E       AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E         At index 2 diff: 'grapes' != 'orange'
E         Use -v to get more diff

test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________

    def test_numbers_fail():
        number_to_text1 = {str(x): x for x in range(5)}
        number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
>       assert number_to_text1 == number_to_text2
E       AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E         Omitting 1 identical items, use -vv to show
E         Left contains 4 more items:
E         {'1': 1, '2': 2, '3': 3, '4': 4}
E         Right contains 4 more items:
E         {'10': 10, '20': 20, '30': 30, '40': 40}
E         Use -v to get more diff

test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________

    def test_long_text_fail():
        long_text = "Lorem ipsum dolor sit amet " * 10
>       assert "hello world" in long_text
E       AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ips... sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '

test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================

Обратите внимание на это:

  • Каждый тест внутри файла отображается одним символом в выводе: . - прохождение, F - неудача.

  • test_words_fail не удалось, и нам показывают краткое резюме, указывающее на то, что индекс 2 двух списков отличается.

  • test_numbers_fail не удалось, и нам показана сводка различий между левыми и правыми элементами словаря. Одинаковые элементы опущены.

  • test_long_text_fail не удалось, и правая часть оператора in усекается с помощью ...`, поскольку ее длина превышает внутренний порог (240 символов в настоящее время).

Теперь мы можем увеличить многословность pytest:

$ pytest --no-header -v
=========================== test session starts ============================
collecting ... collected 4 items

test_verbosity_example.py::test_ok PASSED                            [ 25%]
test_verbosity_example.py::test_words_fail FAILED                    [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED                  [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED                [100%]

================================= FAILURES =================================
_____________________________ test_words_fail ______________________________

    def test_words_fail():
        fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
        fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
>       assert fruits1 == fruits2
E       AssertionError: assert ['banana', 'a...elon', 'kiwi'] == ['banana', 'a...elon', 'kiwi']
E         At index 2 diff: 'grapes' != 'orange'
E         Full diff:
E         - ['banana', 'apple', 'orange', 'melon', 'kiwi']
E         ?                      ^  ^^
E         + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
E         ?                      ^  ^ +

test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________

    def test_numbers_fail():
        number_to_text1 = {str(x): x for x in range(5)}
        number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
>       assert number_to_text1 == number_to_text2
E       AssertionError: assert {'0': 0, '1':..., '3': 3, ...} == {'0': 0, '10'...'30': 30, ...}
E         Omitting 1 identical items, use -vv to show
E         Left contains 4 more items:
E         {'1': 1, '2': 2, '3': 3, '4': 4}
E         Right contains 4 more items:
E         {'10': 10, '20': 20, '30': 30, '40': 40}
E         Full diff:
E         - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
E
E         ...Full output truncated (3 lines hidden), use '-vv' to show

test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________

    def test_long_text_fail():
        long_text = "Lorem ipsum dolor sit amet " * 10
>       assert "hello world" in long_text
E       AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '

test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================

Обратите внимание на это:

  • Каждый тест внутри файла получает свою собственную строку в выводе.

  • test_words_fail теперь показывает два неудачных списка полностью, в дополнение к тому, какой индекс отличается.

  • test_numbers_fail теперь показывает текстовое различие двух словарей, в усеченном виде.

  • test_long_text_fail больше не усекает правую часть оператора in, поскольку внутренний порог для усечения теперь больше (2400 символов в настоящее время).

Теперь, если мы еще больше увеличим многословность:

$ pytest --no-header -vv
=========================== test session starts ============================
collecting ... collected 4 items

test_verbosity_example.py::test_ok PASSED                            [ 25%]
test_verbosity_example.py::test_words_fail FAILED                    [ 50%]
test_verbosity_example.py::test_numbers_fail FAILED                  [ 75%]
test_verbosity_example.py::test_long_text_fail FAILED                [100%]

================================= FAILURES =================================
_____________________________ test_words_fail ______________________________

    def test_words_fail():
        fruits1 = ["banana", "apple", "grapes", "melon", "kiwi"]
        fruits2 = ["banana", "apple", "orange", "melon", "kiwi"]
>       assert fruits1 == fruits2
E       AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
E         At index 2 diff: 'grapes' != 'orange'
E         Full diff:
E         - ['banana', 'apple', 'orange', 'melon', 'kiwi']
E         ?                      ^  ^^
E         + ['banana', 'apple', 'grapes', 'melon', 'kiwi']
E         ?                      ^  ^ +

test_verbosity_example.py:8: AssertionError
____________________________ test_numbers_fail _____________________________

    def test_numbers_fail():
        number_to_text1 = {str(x): x for x in range(5)}
        number_to_text2 = {str(x * 10): x * 10 for x in range(5)}
>       assert number_to_text1 == number_to_text2
E       AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E         Common items:
E         {'0': 0}
E         Left contains 4 more items:
E         {'1': 1, '2': 2, '3': 3, '4': 4}
E         Right contains 4 more items:
E         {'10': 10, '20': 20, '30': 30, '40': 40}
E         Full diff:
E         - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E         ?            -    -    -    -    -    -    -    -
E         + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}

test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________

    def test_long_text_fail():
        long_text = "Lorem ipsum dolor sit amet " * 10
>       assert "hello world" in long_text
E       AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '

test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================

Обратите внимание на это:

  • Каждый тест внутри файла получает свою собственную строку в выводе.

  • test_words_fail в этом случае дает тот же результат, что и раньше.

  • test_numbers_fail теперь показывает полнотекстовое различие двух словарей.

  • test_long_text_fail также не усекается справа, как раньше, но теперь pytest не усекает вообще никакой текст, независимо от его размера.

Это были примеры того, как многословность влияет на обычный вывод тестовой сессии, но многословность используется и в других ситуациях, например, если вы используете _, то вам будут показаны даже фикстуры, начинающиеся с pytest --fixtures -v.

Использование более высоких уровней многословности (-vvv, -vvvv, …) поддерживается, но не имеет никакого эффекта в самом pytest на данный момент, однако некоторые плагины могут использовать более высокий уровень многословности.

Подготовка подробного краткого отчета

Флаг -r можно использовать для отображения «краткой информации о тесте» в конце сеанса тестирования, что позволяет в больших наборах тестов получить четкую картину всех неудач, пропусков, xfails и т.д.

По умолчанию для перечисления сбоев и ошибок используется значение fE.

Пример:

# content of test_example.py
import pytest


@pytest.fixture
def error_fixture():
    assert 0


def test_ok():
    print("ok")


def test_fail():
    assert 0


def test_error(error_fixture):
    pass


def test_skip():
    pytest.skip("skipping this test")


def test_xfail():
    pytest.xfail("xfailing this test")


@pytest.mark.xfail(reason="always xfail")
def test_xpass():
    pass
$ pytest -ra
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 6 items

test_example.py .FEsxX                                               [100%]

================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________

    @pytest.fixture
    def error_fixture():
>       assert 0
E       assert 0

test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________

    def test_fail():
>       assert 0
E       assert 0

test_example.py:14: AssertionError
========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail - reason: xfailing this test
XPASS test_example.py::test_xpass always xfail
ERROR test_example.py::test_error - assert 0
FAILED test_example.py::test_fail - assert 0
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===

Опции -r принимают число символов после него, при этом a, использованный выше, означает «все, кроме пропусков».

Вот полный список доступных символов, которые можно использовать:

  • f - не удалось

  • E - ошибка

  • s - пропущен

  • x - xfailed

  • X - пройдено

  • p - пройдено

  • P - передается с выходом

Специальные символы для (де)выбора групп:

  • a - все, кроме pP

  • A - все

  • N - none, может использоваться для отображения ничего (так как fE используется по умолчанию)

Можно использовать более одного символа, поэтому, например, чтобы увидеть только неудачные и пропущенные тесты, можно выполнить:

$ pytest -rfs
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 6 items

test_example.py .FEsxX                                               [100%]

================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________

    @pytest.fixture
    def error_fixture():
>       assert 0
E       assert 0

test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________

    def test_fail():
>       assert 0
E       assert 0

test_example.py:14: AssertionError
========================= short test summary info ==========================
FAILED test_example.py::test_fail - assert 0
SKIPPED [1] test_example.py:22: skipping this test
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===

Использование p перечисляет прошедшие тесты, в то время как P добавляет дополнительный раздел «PASSES» с теми тестами, которые прошли, но имели захваченный вывод:

$ pytest -rpP
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 6 items

test_example.py .FEsxX                                               [100%]

================================== ERRORS ==================================
_______________________ ERROR at setup of test_error _______________________

    @pytest.fixture
    def error_fixture():
>       assert 0
E       assert 0

test_example.py:6: AssertionError
================================= FAILURES =================================
________________________________ test_fail _________________________________

    def test_fail():
>       assert 0
E       assert 0

test_example.py:14: AssertionError
================================== PASSES ==================================
_________________________________ test_ok __________________________________
--------------------------- Captured stdout call ---------------------------
ok
========================= short test summary info ==========================
PASSED test_example.py::test_ok
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===

Создание файлов формата журнала результатов

Для создания машиночитаемых файлов результатов в виде обычного текста вы можете выдать команду:

pytest --resultlog=path

и посмотрите содержимое в месте path. Такие файлы используются, например, на веб-странице PyPy-test для отображения результатов тестирования за несколько ревизий.

Предупреждение

Эта опция используется редко и планируется к удалению в pytest 6.0.

Если вы используете эту опцию, подумайте о том, чтобы вместо нее использовать новый плагин pytest-reportlog.

Для получения дополнительной информации см. раздел the deprecation docs.

Создание файлов формата JUnitXML

Чтобы создать файлы результатов, которые могут быть прочитаны Jenkins или другими серверами непрерывной интеграции, используйте это приглашение:

pytest --junitxml=path

для создания XML-файла по адресу path.

Чтобы задать имя корневого элемента xml набора тестов, вы можете настроить опцию junit_suite_name в вашем конфигурационном файле:

[pytest]
junit_suite_name = my_suite

Добавлено в версии 4.0.

Спецификация JUnit XML, похоже, указывает, что атрибут "time" должен сообщать общее время выполнения теста, включая установку и разрыв (1, 2). Это поведение pytest по умолчанию. Чтобы вместо этого сообщать только длительность вызовов, настройте параметр junit_duration_report следующим образом:

[pytest]
junit_duration_report = call

свойство_записи

Если вы хотите зарегистрировать дополнительную информацию для теста, вы можете использовать приспособление record_property:

def test_function(record_property):
    record_property("example_key", 1)
    assert True

Это добавит дополнительное свойство example_key="1" к сгенерированному тегу testcase:

<testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
  <properties>
    <property name="example_key" value="1" />
  </properties>
</testcase>

Кроме того, вы можете интегрировать эту функциональность с пользовательскими маркерами:

# content of conftest.py


def pytest_collection_modifyitems(session, config, items):
    for item in items:
        for marker in item.iter_markers(name="test_id"):
            test_id = marker.args[0]
            item.user_properties.append(("test_id", test_id))

И в ваших тестах:

# content of test_function.py
import pytest


@pytest.mark.test_id(1501)
def test_function():
    assert True

В результате:

<testcase classname="test_function" file="test_function.py" line="0" name="test_function" time="0.0009">
  <properties>
    <property name="test_id" value="1501" />
  </properties>
</testcase>

Предупреждение

Обратите внимание, что использование этой функции нарушит проверку схемы для последней версии схемы JUnitXML. Это может стать проблемой при использовании с некоторыми серверами CI.

атрибут записи_xml_attribute

Чтобы добавить дополнительный xml-атрибут к элементу testcase, можно использовать приспособление record_xml_attribute. Это также может быть использовано для переопределения существующих значений:

def test_function(record_xml_attribute):
    record_xml_attribute("assertions", "REQ-1234")
    record_xml_attribute("classname", "custom_classname")
    print("hello world")
    assert True

В отличие от record_property, это не добавит новый дочерний элемент. Вместо этого будет добавлен атрибут assertions="REQ-1234" внутри сгенерированного тега testcase и заменен стандартный classname на "classname=custom_classname":

<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
    <system-out>
        hello world
    </system-out>
</testcase>

Предупреждение

record_xml_attribute является экспериментальной функцией, и ее интерфейс может быть заменен на что-то более мощное и общее в будущих версиях. Однако функциональность как таковая будет сохранена.

Использование этого параметра вместо record_xml_property может помочь при использовании инструментов ci для разбора отчета xml. Однако некоторые анализаторы довольно строги к элементам и атрибутам, которые разрешены. Многие инструменты используют схему xsd (как в примере ниже) для проверки входящего xml. Убедитесь, что вы используете имена атрибутов, которые разрешены вашим анализатором.

Ниже приведена схема, используемая Jenkins для проверки отчета XML:

<xs:element name="testcase">
    <xs:complexType>
        <xs:sequence>
            <xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
            <xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
            <xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
            <xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
            <xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
        </xs:sequence>
        <xs:attribute name="name" type="xs:string" use="required"/>
        <xs:attribute name="assertions" type="xs:string" use="optional"/>
        <xs:attribute name="time" type="xs:string" use="optional"/>
        <xs:attribute name="classname" type="xs:string" use="optional"/>
        <xs:attribute name="status" type="xs:string" use="optional"/>
    </xs:complexType>
</xs:element>

Предупреждение

Обратите внимание, что использование этой функции нарушит проверку схемы для последней версии схемы JUnitXML. Это может стать проблемой при использовании с некоторыми серверами CI.

запись_тестуемого_свойства

Добавлено в версии 4.5.

Если вы хотите добавить узел свойств на уровне набора тестов, который может содержать свойства, относящиеся ко всем тестам, вы можете использовать привязку record_testsuite_property, скопированную на сессию:

Приспособление record_testsuite_property, скопированное на сессию, можно использовать для добавления свойств, относящихся ко всем тестам.

import pytest


@pytest.fixture(scope="session", autouse=True)
def log_global_env_facts(record_testsuite_property):
    record_testsuite_property("ARCH", "PPC")
    record_testsuite_property("STORAGE_TYPE", "CEPH")


class TestMe:
    def test_foo(self):
        assert True

Приспособление представляет собой вызываемый объект, который получает name и value из тега <property>, добавленного на уровне тестового набора сгенерированного xml:

<testsuite errors="0" failures="0" name="pytest" skipped="0" tests="1" time="0.006">
  <properties>
    <property name="ARCH" value="PPC"/>
    <property name="STORAGE_TYPE" value="CEPH"/>
  </properties>
  <testcase classname="test_me.TestMe" file="test_me.py" line="16" name="test_foo" time="0.000243663787842"/>
</testsuite>

name должен быть строкой, value будет преобразован в строку и надлежащим образом xml-escaped.

Генерируемый XML совместим с последним стандартом xunit, в отличие от record_property и record_xml_attribute.

Отправка отчета о тестировании в онлайн-сервис pastebin

Создание URL-адреса для каждого сбоя теста:

pytest --pastebin=failed

Это отправит информацию о выполнении тестов на удаленный сервис Paste и предоставит URL для каждого сбоя. Вы можете выбрать тесты как обычно или добавить, например, -x, если вы хотите отправить только один конкретный сбой.

Создание URL-адреса для всего журнала тестовой сессии:

pytest --pastebin=all

В настоящее время реализована только вставка в сервис https://bpaste.net/.

Изменено в версии 5.2.

Если создание URL не удается по какой-либо причине, вместо отказа всего набора тестов выдается предупреждение.

Вернуться на верх