Как писать и сообщать о проверках в тестах
Проверка с помощью оператора assert
pytest позволяет использовать стандартный assert Python для проверки ожиданий и значений в тестах 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-9.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"
оно выводится вместе с информацией интроспекции проверки в трассировке.
Подробнее об интроспекции проверок см. в разделе Подробности интроспекции проверок.
Проверки приблизительного равенства
При сравнении чисел с плавающей точкой (или массивов таких чисел) часто возникают небольшие ошибки округления. Вместо assert abs(a - b) < tol или numpy.isclose можно использовать pytest.approx():
import pytest
import numpy as np
def test_floats():
assert (0.1 + 0.2) == pytest.approx(0.3)
def test_arrays():
a = np.array([1.0, 2.0, 3.0])
b = np.array([0.9999, 2.0001, 3.0])
assert a == pytest.approx(b)
pytest.approx работает со скалярами, списками, словарями и массивами NumPy. Также поддерживаются сравнения с участием NaN.
Подробности см. в разделе pytest.approx().
Проверки ожидаемых исключений
Для проверки возникающих исключений можно использовать 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.
Обратите внимание: pytest.raises соответствует указанному типу исключения или любому его подклассу (как и стандартный оператор except). Если нужно проверить, что блок кода вызывает исключение точно указанного типа, это следует проверить явно:
def test_foo_not_implemented():
def foo():
raise NotImplementedError
with pytest.raises(RuntimeError) as excinfo:
foo()
assert excinfo.type is RuntimeError
Вызов pytest.raises() завершится успешно, хотя функция вызывает NotImplementedError, поскольку NotImplementedError является подклассом RuntimeError; однако следующий оператор assert обнаружит эту проблему.
Сопоставление сообщений исключений
Менеджеру контекста можно передать именованный параметр match, чтобы проверить соответствие регулярного выражения строковому представлению исключения (аналогично методу TestCase.assertRaisesRegex из unittest):
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'. - Параметр
matchтакже сопоставляется с__notes__из PEP-678.
Проверки ожидаемых групп исключений
Если ожидается BaseExceptionGroup или ExceptionGroup, можно использовать pytest.RaisesGroup:
def test_exception_in_group():
with pytest.RaisesGroup(ValueError):
raise ExceptionGroup("group msg", [ValueError("value msg")])
with pytest.RaisesGroup(ValueError, TypeError):
raise ExceptionGroup("msg", [ValueError("foo"), TypeError("bar")])
Он принимает параметр match для проверки сообщения группы и параметр check, которому передаётся произвольный вызываемый объект. Проверка пройдёт, только если вызываемый объект вернёт True.
def test_raisesgroup_match_and_check():
with pytest.RaisesGroup(BaseException, match="my group msg"):
raise BaseExceptionGroup("my group msg", [KeyboardInterrupt()])
with pytest.RaisesGroup(
Exception, check=lambda eg: isinstance(eg.__cause__, ValueError)
):
raise ExceptionGroup("", [TypeError()]) from ValueError()
Этот способ строго проверяет структуру и необёрнутые исключения, в отличие от except*, поэтому может понадобиться задать параметры flatten_subgroups и/или allow_unwrapped.
def test_structure():
with pytest.RaisesGroup(pytest.RaisesGroup(ValueError)):
raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),))
with pytest.RaisesGroup(ValueError, flatten_subgroups=True):
raise ExceptionGroup("1st group", [ExceptionGroup("2nd group", [ValueError()])])
with pytest.RaisesGroup(ValueError, allow_unwrapped=True):
raise ValueError
Чтобы задать дополнительные сведения о содержащемся исключении, можно использовать pytest.RaisesExc
def test_raises_exc():
with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="foo")):
raise ExceptionGroup("", (ValueError("foo")))
У обоих объектов есть методы pytest.RaisesGroup.matches() pytest.RaisesExc.matches(), позволяющие выполнять сопоставление без использования объекта в качестве менеджера контекста. Это может быть полезно при проверке .__context__ или .__cause__.
def test_matches():
exc = ValueError()
exc_group = ExceptionGroup("", [exc])
if RaisesGroup(ValueError).matches(exc_group):
...
# helpful error is available in `.fail_reason` if it fails to match
r = RaisesExc(ValueError)
assert r.matches(e), r.fail_reason
Дополнительные сведения и примеры см. в документации по pytest.RaisesGroup и pytest.RaisesExc.
ExceptionInfo.group_contains()
Предупреждение
Этот вспомогательный метод упрощает проверку наличия определённых исключений, но совершенно не подходит для проверки того, что группа не содержит никаких других исключений. Поэтому следующая проверка пройдёт:
class EXTREMELYBADERROR(BaseException):
"""This is a very bad error to miss"""
def test_for_value_error():
with pytest.raises(ExceptionGroup) as excinfo:
excs = [ValueError()]
if very_unlucky():
excs.append(EXTREMELYBADERROR())
raise ExceptionGroup("", excs)
# This passes regardless of if there's other exceptions.
assert excinfo.group_contains(ValueError)
# You can't simply list all exceptions you *don't* want to get here.
Не существует надёжного способа использовать excinfo.group_contains(), чтобы убедиться, что в группе нет никаких исключений, кроме ожидаемого. Вместо этого следует использовать pytest.RaisesGroup; см. раздел Проверки ожидаемых групп исключений.
Также для проверки исключений, возвращённых в составе ExceptionGroup, можно использовать метод excinfo.group_contains():
def test_exception_in_group():
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError("Exception 123 raised"),
],
)
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
assert not excinfo.group_contains(TypeError)
Необязательный именованный параметр match работает так же, как и для pytest.raises().
По умолчанию group_contains() рекурсивно ищет подходящее исключение на любом уровне вложенных экземпляров ExceptionGroup. Если нужно сопоставить исключение только на определённом уровне, можно указать именованный параметр depth; исключения, непосредственно содержащиеся в верхней группе ExceptionGroup, будут соответствовать depth=1.
def test_exception_in_group_at_given_depth():
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError(),
ExceptionGroup(
"Nested group",
[
TypeError(),
],
),
],
)
assert excinfo.group_contains(RuntimeError, depth=1)
assert excinfo.group_contains(TypeError, depth=2)
assert not excinfo.group_contains(RuntimeError, depth=2)
assert not excinfo.group_contains(TypeError, depth=1)
Альтернативная форма pytest.raises (устаревшая)
Существует альтернативная форма pytest.raises(): в неё передаётся функция для выполнения, а также *args и **kwargs. Затем pytest.raises() выполнит функцию с этими аргументами и проверит, что возникло заданное исключение:
def func(x):
if x <= 0:
raise ValueError("x needs to be larger than zero")
pytest.raises(ValueError, func, x=-1)
Эта форма была первоначальным API pytest.raises(), созданным до появления в языке Python оператора with. Сейчас эта форма используется редко; более читабельной считается форма с менеджером контекста (с использованием with).
Метка xfail и pytest.raises
Также можно указать аргумент raises для pytest.mark.xfail, чтобы проверять более конкретный способ сбоя теста, а не просто факт возникновения любого исключения:
def f():
raise IndexError()
@pytest.mark.xfail(raises=IndexError)
def test_f():
f()
В этом случае тест будет помечен как «xfail» только при сбое из-за IndexError или его подклассов.
- Использование pytest.mark.xfail с параметром
raises, вероятно, лучше подходит для документирования неисправленных ошибок (когда тест описывает, как «должно» работать) или ошибок в зависимостях. - Использование
pytest.raises(), вероятно, лучше подходит для проверки исключений, которые намеренно вызываются вашим собственным кодом; это наиболее распространённый случай.
Также можно использовать pytest.RaisesGroup:
def f():
raise ExceptionGroup("", [IndexError()])
@pytest.mark.xfail(raises=RaisesGroup(IndexError))
def test_f():
f()
Проверки ожидаемых предупреждений
Проверить, что код выдаёт определённое предупреждение, можно с помощью 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-9.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
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 =============================
Для нескольких типов данных выполняются специальные сравнения:
- сравнение длинных строк: показывается контекстная разница
- сравнение длинных последовательностей: показываются первые индексы с несовпадениями
- сравнение словарей: показываются различающиеся элементы
В контекстных различиях строк строки с префиксом - взяты из левой части assert left == right, а строки с префиксом + — из правой.
Множество других примеров приведено в демонстрации отчётов.
Создание собственных пояснений для неудачных проверок
Можно добавлять собственные подробные пояснения, реализовав хук pytest_assertrepr_compare.
- pytest_assertrepr_compare(config, op, left, right)
-
Возвращает пояснение для сравнений в неудачных выражениях assert.
Если пользовательское пояснение не требуется, верните None; в противном случае верните список строк. Строки будут объединены символами новой строки, но любые символы новой строки внутри строки будут экранированы. Обратите внимание: все строки, кроме первой, будут иметь небольшой отступ; предполагается, что первая строка содержит краткое описание.
- Параметры:
-
- config – Объект конфигурации pytest.
-
op – Оператор, например
"==","!=","not in". - left – Левый операнд.
- right – Правый операнд.
Использование в плагинах conftest
Этот хук можно реализовать в любом файле conftest. Для заданного элемента проверяются только файлы conftest в каталоге этого элемента и его родительских каталогах.
Например, можно добавить следующий хук в файл 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
Возврат значения, отличного от None, из тестовых функций
Если тестовая функция возвращает значение, отличное от None, выдаётся предупреждение pytest.PytestReturnNotNoneWarning.
Это помогает предотвратить распространённую ошибку новичков, которые полагают, что возврат bool (например, True или False) определяет, пройдёт тест или нет.
Пример:
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
return foo(a, b) == result # Incorrect usage, do not do this.
Поскольку pytest игнорирует возвращаемые значения, может показаться неожиданным, что тест никогда не завершится ошибкой из-за возвращённого значения.
Правильное решение — заменить оператор return на assert:
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
assert foo(a, b) == result
Подробности интроспекции проверок
Подробности о неудачной проверке выводятся благодаря переписыванию операторов assert до их выполнения. Переписанные операторы assert добавляют информацию интроспекции в сообщение об ошибке проверки. pytest переписывает только тестовые модули, непосредственно обнаруженные в процессе сбора тестов, поэтому операторы assert во вспомогательных модулях, которые сами не являются тестовыми модулями, переписаны не будут.
Можно вручную включить переписывание проверок для импортируемого модуля, вызвав register_assert_rewrite до его импорта (удобно сделать это в корневом conftest.py).
Дополнительные сведения можно найти в статье Бенджамина Петерсона Как устроено новое переписывание проверок в pytest.
Переписывание проверок кэширует файлы на диске
pytest записывает переписанные модули на диск для кэширования. Это поведение можно отключить (например, чтобы в проектах, где файлы часто перемещаются, не оставались устаревшие файлы .pyc), добавив следующую строку в начало файла conftest.py:
import sys sys.dont_write_bytecode = True
Обратите внимание: преимущества интроспекции проверок сохраняются; меняется только то, что файлы .pyc не кэшируются на диске.
Кроме того, переписывание молча пропускает кэширование, если не удаётся записать новые файлы .pyc, например в файловой системе только для чтения или в zip-файле.
Отключение переписывания assert
pytest переписывает тестовые модули при импорте, используя хук импорта для создания новых файлов pyc. В большинстве случаев это происходит прозрачно. Однако при самостоятельной работе с механизмом импорта хук импорта может мешать.
В этом случае есть два варианта:
- Отключить переписывание для конкретного модуля, добавив строку
PYTEST_DONT_REWRITEв его строку документации. - Отключить переписывание для всех модулей с помощью
--assert=plain.
© 2015–2026 Holger Krekel and pytest-dev team
Licensed under the MIT License.
https://docs.pytest.org/en/stable/how-to/assert.html