Как писать и сообщать об утверждениях в тестах

Утверждение с помощью оператора assert

pytest позволяет вам использовать стандартный Python assert для проверки ожиданий и значений в тестах Python. Например, вы можете написать следующее:

# content of test_assert1.py
def f():
    return 3


def test_function():
    assert f() == 4

утверждать, что ваша функция возвращает определенное значение. Если это утверждение не проходит, вы увидите возвращаемое значение вызова функции:

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

test_assert1.py F                                                    [100%]

================================= FAILURES =================================
______________________________ test_function _______________________________

    def test_function():
>       assert f() == 4
E       assert 3 == 4
E        +  where 3 = f()

test_assert1.py:6: AssertionError
========================= short test summary info ==========================
FAILED test_assert1.py::test_function - assert 3 == 4
============================ 1 failed in 0.12s =============================

pytest поддерживает отображение значений наиболее распространенных подвыражений, включая вызовы, атрибуты, сравнения, двоичные и унарные операторы. (См. Демонстрация отчетов об отказах в Python с помощью pytest). Это позволяет использовать идиоматические конструкции python без кодового шаблона, не теряя при этом информацию интроспекции.

Однако, если вы укажете сообщение с утверждением следующим образом:

assert a % 2 == 0, "value was odd, should be even"

то никакой интроспекции утверждений не происходит вообще, и сообщение будет просто показано в трассировке.

Дополнительную информацию о интроспекции утверждений см. в Детали интроспекции утверждений.

Утверждения об ожидаемых исключениях

Для того чтобы писать утверждения о поднятых исключениях, вы можете использовать pytest.raises() в качестве менеджера контекста следующим образом:

import pytest


def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

и если вам нужен доступ к фактической информации об исключении, вы можете использовать:

def test_recursion_depth():
    with pytest.raises(RuntimeError) as excinfo:

        def f():
            f()

        f()
    assert "maximum recursion" in str(excinfo.value)

excinfo представляет собой экземпляр ExceptionInfo, который является оберткой вокруг фактического поднятого исключения. Основными атрибутами, представляющими интерес, являются .type, .value и .traceback.

Вы можете передать ключевой параметр match в контекст-менеджер для проверки соответствия регулярного выражения строковому представлению исключения (аналогично методу TestCase.assertRaisesRegex из unittest):

import pytest


def myfunc():
    raise ValueError("Exception 123 raised")


def test_match():
    with pytest.raises(ValueError, match=r".* 123 .*"):
        myfunc()

Параметр regexp метода match сопоставляется с функцией re.search, поэтому в приведенном выше примере match='123' сработал бы также.

Существует альтернативная форма функции pytest.raises(), в которой вы передаете функцию, которая будет выполнена с заданными *args и **kwargs и утверждаете, что возникло заданное исключение:

pytest.raises(ExpectedException, func, *args, **kwargs)

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

Обратите внимание, что также можно указать аргумент «raises» для pytest.mark.xfail, который проверяет, что тест терпит неудачу более специфическим образом, чем просто поднятие любого исключения:

@pytest.mark.xfail(raises=IndexError)
def test_f():
    f()

Использование pytest.raises(), вероятно, лучше для случаев, когда вы тестируете исключения, которые намеренно вызывает ваш собственный код, тогда как использование @pytest.mark.xfail с функцией проверки, вероятно, лучше для таких случаев, как документирование неисправленных ошибок (когда тест описывает, что «должно» произойти) или ошибок в зависимостях.

Утверждения об ожидаемых предупреждениях

Вы можете проверить, что код вызывает определенное предупреждение, используя pytest.warns.

Использование контекстно-зависимых сравнений

pytest имеет богатую поддержку для предоставления контекстно-зависимой информации при встрече со сравнениями. Например:

# content of test_assert2.py
def test_set_comparison():
    set1 = set("1308")
    set2 = set("8035")
    assert set1 == set2

если вы запустите этот модуль:

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

test_assert2.py F                                                    [100%]

================================= FAILURES =================================
___________________________ test_set_comparison ____________________________

    def test_set_comparison():
        set1 = set("1308")
        set2 = set("8035")
>       assert set1 == set2
E       AssertionError: assert {'0', '1', '3', '8'} == {'0', '3', '5', '8'}
E         Extra items in the left set:
E         '1'
E         Extra items in the right set:
E         '5'
E         Use -v to get more diff

test_assert2.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_assert2.py::test_set_comparison - AssertionError: assert {'0'...
============================ 1 failed in 0.12s =============================

Для ряда случаев проводятся специальные сравнения:

  • сравнение длинных строк: отображается контекстное различие

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

  • сравнение dicts: разные записи

Больше примеров смотрите в reporting demo.

Определение собственного объяснения неудачных утверждений

Можно добавить свои собственные подробные объяснения, применив хук pytest_assertrepr_compare.

pytest_assertrepr_compare(config, op, left, right)[исходный код]

Возвращает объяснение для сравнений в неработающих выражениях assert.

Возвращает None для отсутствия пользовательского объяснения, в противном случае возвращает список строк. Строки будут объединены новыми строками, но любые новые строки в строке будут экранированы. Обратите внимание, что все строки, кроме первой, будут иметь небольшой отступ, поскольку первая строка должна быть кратким изложением.

Parameters:
  • config (Config) – Объект конфигурации pytest.

  • op (str) – Оператор, например, "==", "!=", "not in".

  • left (object) – Левый операнд.

  • right (object) – Правый операнд.

В качестве примера рассмотрим добавление следующего хука в файл conftest.py, который предоставляет альтернативное объяснение для объектов Foo:

# content of conftest.py
from test_foocompare import Foo


def pytest_assertrepr_compare(op, left, right):
    if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
        return [
            "Comparing Foo instances:",
            f"   vals: {left.val} != {right.val}",
        ]

теперь, учитывая этот тестовый модуль:

# content of test_foocompare.py
class Foo:
    def __init__(self, val):
        self.val = val

    def __eq__(self, other):
        return self.val == other.val


def test_compare():
    f1 = Foo(1)
    f2 = Foo(2)
    assert f1 == f2

вы можете запустить модуль тестирования и получить пользовательский вывод, определенный в файле conftest:

$ pytest -q test_foocompare.py
F                                                                    [100%]
================================= FAILURES =================================
_______________________________ test_compare _______________________________

    def test_compare():
        f1 = Foo(1)
        f2 = Foo(2)
>       assert f1 == f2
E       assert Comparing Foo instances:
E            vals: 1 != 2

test_foocompare.py:12: AssertionError
========================= short test summary info ==========================
FAILED test_foocompare.py::test_compare - assert Comparing Foo instances:
1 failed in 0.12s

Детали интроспекции утверждений

Сообщение подробностей о неудачном утверждении достигается путем переписывания утверждений перед их выполнением. Переписанные утверждения assert помещают информацию интроспекции в сообщение о неудаче утверждения. pytest переписывает только тестовые модули, непосредственно обнаруженные в процессе сбора тестов, поэтому утверждения во вспомогательных модулях, которые сами не являются тестовыми модулями, не будут переписаны.

Вы можете вручную включить перезапись утверждений для импортируемого модуля, вызвав register_assert_rewrite перед импортом (хорошее место для этого - в корне conftest.py).

Для получения дополнительной информации Бенджамин Петерсон написал Behind the scenes of pytest’s new assertion rewriting.

Перезапись утверждений кэширует файлы на диске

pytest будет записывать переписанные модули на диск для кэширования. Вы можете отключить это поведение (например, чтобы не оставлять неактуальные файлы .pyc в проектах, которые часто перемещают файлы), добавив это в начало вашего файла conftest.py:

import sys

sys.dont_write_bytecode = True

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

Кроме того, перезапись будет молча пропускать кэширование, если она не может записать новые .pyc файлы, т.е. в файловой системе только для чтения или в zip-файле.

Отключение перезаписи утверждений

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

В этом случае у вас есть два варианта:

  • Отключите переписывание для определенного модуля, добавив строку PYTEST_DONT_REWRITE в его docstring.

  • Отключите перезапись для всех модулей, используя --assert=plain.

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