Оператор assert
и вывод информации о проверках¶
Проверка с помощью оператора assert
¶
pytest
позволяет использовать стандартный оператор языка Python - assert
-
для проверки соответствия ожидаемых результатов фактическим. Например, такую конструкцию
# content of test_assert1.py
def f():
return 3
def test_function():
assert f() == 4
можно использовать, чтобы убедиться что ваша функция вернет определенное значение.
Если assert
упадет, вы сможете увидеть значение, возвращаемое вызванной функцией:
$ pytest test_assert1.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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
============================ 1 failed in 0.12s =============================
pytest
поддерживает отображение значений наиболее распространенных операций, включая
вызовы, параметры, сравнения, бинарные и унарные операции (см. Python: примеры отчетов об ошибках pytest).
Это позволяет использовать стандартные конструкции python
без шаблонного кода,
не теряя при этом информацию.
Однако, если вы укажете в assert
текст сообщения об ошибке, например, вот так,
assert a % 2 == 0, "value was odd, should be even"
то никакая аналитическая информация выводиться не будет, и в трейсбэке вы увидите просто указанное сообщение об ошибке.
См. Детальный анализ неудачных проверок (assertion introspection) для получения дополнительной информации об анализе операторов assert
.
Проверка ожидаемых исключений¶
Чтобы убедиться в том, что вызвано ожидаемое исключение, нужно использовать
assert
в контексте 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
.
Чтобы проверить, что регулярное выражение соответствует строковому представлению исключения
(аналогично методу TestCase.assertRaisesRegexp
в unittest
),
конекст-менеджеру можно передать параметр match
:
import pytest
def myfunc():
raise ValueError("Exception 123 raised")
def test_match():
with pytest.raises(ValueError, match=r".* 123 .*"):
myfunc()
Регулярное выражение из параметра match
сопоставляется с функцией re.search
,
так что в приведенном выше примере match='123'
также сработает.
Есть и альтернативный вариант использования pytest.raises
, когда вы передаете функцию, которая
которая должна выполняться с заданными *args
и **kwargs
и проверять, что вызвано указанное исключение:
pytest.raises(ExpectedException, func, *args, **kwargs)
В случае падения теста pytest
выведет вам полезную информацию, например, о том, что
исключение не вызвано (no exception) или вызвано неверное исключение (wrong exception).
Обратите внимание, что параметр 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-5.x.y, py-1.x.y, pluggy-0.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
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 the full diff
test_assert2.py:6: AssertionError
============================ 1 failed in 0.12s =============================
Вывод результатов сравнения для отдельных случаев:
сравнение длинных строк: будут показаны различия
сравнение длинных последовательностей: будет показан индекс первого несоответствия
сравнение словарей: будут показаны различающиеся элементы
Больше примеров: reporting demo.
Определение собственных сообщений к упавшим assert
¶
Можно добавить свое подробное объяснение, реализовав хук (hook) pytest_assertrepr_compare
(см. pytest_assertrepr_compare).
Для примера рассмотрим добавление в файл 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:",
" vals: {} != {}".format(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.py
:
$ 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
1 failed in 0.12s
Детальный анализ неудачных проверок (assertion introspection)¶
Детальный анализ упавших проверок достигается переопределением операторов assert
перед запуском.
Переопределенные assert
помещают аналитическую информацию в сообщение о неудачной проверке.
pytest
переопределяет только тестовые модули, обнаруженные им в процессе сборки (collecting)
тестов, поэтому ``assert``-ы в поддерживающих модулях, которые сами по себе не являются тестами,
переопределены не будут.
Можно вручную включить возможность переопределения assert
для импортируемого модуля, вызвав
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
- файлы, т. е. для read-only файлов или zip-архивов.
Отключение переопределения assert
¶
При импорте pytest
перезаписывает тестовые модули, используя хук импорта для записи новых
.pyc
-файлов. В большинстве случаев это работает. Тем не менее, при работе с механизмом импорта,
такой способ может создавать проблемы.
На этот случай есть 2 опции:
Отключите переопределение для отдельного модуля, добавив строку
PYTEST_DONT_REWRITE
в docstring (строковую переменную для документирования модуля).Отключите переопределение для всех модулей с помощью
--assert=plain
.