Как перехватывать предупреждения
Начиная с версии 3.1, pytest автоматически перехватывает предупреждения во время выполнения тестов и отображает их в конце сеанса:
# content of test_show_warnings.py
import warnings
def api_v1():
warnings.warn(UserWarning("api v1, should use functions from v2"))
return 1
def test_one():
assert api_v1() == 1
Теперь при запуске pytest выводится следующее:
$ pytest test_show_warnings.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_show_warnings.py . [100%]
============================= warnings summary =============================
test_show_warnings.py::test_one
/home/sweet/project/test_show_warnings.py:5: UserWarning: api v1, should use functions from v2
warnings.warn(UserWarning("api v1, should use functions from v2"))
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================= 1 passed, 1 warning in 0.12s =======================
Управление предупреждениями
Как и фильтр предупреждений Python и флагом -W option, pytest предоставляет собственный флаг -W для управления тем, какие предупреждения игнорируются, отображаются или преобразуются в ошибки. Более сложные варианты использования описаны в документации по фильтру предупреждений.
В этом примере кода показано, как считать предупреждение любой категории UserWarning ошибкой:
$ pytest -q test_show_warnings.py -W error::UserWarning
F [100%]
================================= FAILURES =================================
_________________________________ test_one _________________________________
def test_one():
> assert api_v1() == 1
^^^^^^^^
test_show_warnings.py:10:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def api_v1():
> warnings.warn(UserWarning("api v1, should use functions from v2"))
E UserWarning: api v1, should use functions from v2
test_show_warnings.py:5: UserWarning
========================= short test summary info ==========================
FAILED test_show_warnings.py::test_one - UserWarning: api v1, should use ...
1 failed in 0.12s
Тот же параметр можно задать в файле конфигурации с помощью параметра конфигурации filterwarnings. Например, приведенная ниже конфигурация игнорирует все пользовательские предупреждения и конкретные предупреждения об устаревании, соответствующие регулярному выражению, а все остальные предупреждения преобразует в ошибки.
toml
[pytest]
filterwarnings = [
'error',
'ignore::UserWarning',
# Note the use of single quote below to denote "raw" strings in TOML.
'ignore:function ham\(\) is deprecated:DeprecationWarning',
]
ini
[pytest]
filterwarnings =
error
ignore::UserWarning
ignore:function ham\(\) is deprecated:DeprecationWarning
Если предупреждение соответствует нескольким параметрам из списка, применяется действие, указанное в последнем подходящем параметре.
Примечание
Флаг -W и параметр конфигурации filterwarnings используют фильтры предупреждений схожей структуры, но каждый параметр конфигурации интерпретирует свой фильтр по-разному. Например, message в filterwarnings — это строка с регулярным выражением, которому должно соответствовать начало текста предупреждения без учета регистра, а message в -W — это буквальная строка, которая должна содержаться в начале текста предупреждения (без учета регистра); пробелы в начале и конце текста игнорируются. Подробнее см. в документации по фильтру предупреждений.
@pytest.mark.filterwarnings
С помощью метки @pytest.mark.filterwarnings можно добавлять фильтры предупреждений для отдельных тестовых элементов. Это позволяет точнее управлять тем, какие предупреждения следует перехватывать на уровне теста, класса и даже модуля:
import warnings
def api_v1():
warnings.warn(UserWarning("api v1, should use functions from v2"))
return 1
@pytest.mark.filterwarnings("ignore:api v1")
def test_one():
assert api_v1() == 1
Можно указать несколько фильтров с помощью отдельных декораторов:
# Ignore "api v1" warnings, but fail on all other warnings
@pytest.mark.filterwarnings("ignore:api v1")
@pytest.mark.filterwarnings("error")
def test_one():
assert api_v1() == 1
Также можно передать несколько фильтров одной метке, указав несколько аргументов:
# Later arguments take precedence, matching warnings.filterwarnings behavior.
@pytest.mark.filterwarnings("error", "ignore:api v1")
def test_one():
assert api_v1() == 1
Важно
О порядке декораторов и приоритете фильтров: важно помнить, что декораторы вычисляются в обратном порядке, поэтому фильтры предупреждений нужно перечислять в порядке, обратном традиционному использованию warnings.filterwarnings() и -W option. На практике это означает, что фильтры из более ранних декораторов @pytest.mark.filterwarnings имеют приоритет над фильтрами из более поздних декораторов, как показано в примере выше.
Фильтры, примененные с помощью метки, имеют приоритет над фильтрами, переданными в командной строке или заданными параметром конфигурации filterwarnings.
Чтобы применить фильтр ко всем тестам класса, используйте метку filterwarnings как декоратор класса. Чтобы применить фильтр ко всем тестам модуля, задайте переменную pytestmark:
# turns all warnings into errors for this module
pytestmark = pytest.mark.filterwarnings("error")
Примечание
Если нужно применить несколько фильтров (присвоив список меток filterwarnings переменной pytestmark), следует использовать традиционный подход к порядку warnings.filterwarnings() (приоритет имеют более поздние фильтры). Это противоположно описанному выше подходу с декораторами.
Благодарим Флориана Шульце за эталонную реализацию в плагине pytest-warnings.
Установка максимального количества предупреждений
Добавлено в версии 9.1.
С помощью параметра командной строки --max-warnings можно завершить запуск тестов с ошибкой, если общее количество предупреждений превысит заданный порог:
pytest --max-warnings=10
Если все тесты пройдены, но количество предупреждений превышает порог, pytest завершится с кодом 6 (ExitCode MAX_WARNINGS_ERROR). Это полезно, чтобы постепенно сокращать количество предупреждений в кодовой базе.
Обратите внимание: filtered warnings не учитываются при подсчете общего количества для этого порога.
Порог также можно задать в файле конфигурации с помощью max_warnings:
toml
[pytest] max_warnings = 10
ini
[pytest] max_warnings = 10
Примечание
Если тесты завершаются с ошибкой, код завершения будет 1 (ExitCode TESTS_FAILED) независимо от количества предупреждений. MAX_WARNINGS_ERROR сообщается только в том случае, если все тесты пройдены, но порог предупреждений превышен.
Отключение сводки предупреждений
Хотя это и не рекомендуется, можно использовать параметр командной строки --disable-warnings, чтобы полностью убрать сводку предупреждений из вывода запуска тестов.
Полное отключение перехвата предупреждений
Этот плагин включен по умолчанию, но его можно полностью отключить в файле конфигурации:
toml
[pytest] addopts = ["-p", "no:warnings"]
ini
[pytest] addopts = -p no:warnings
Либо передайте -p no:warnings в командной строке. Это может пригодиться, если набор тестов обрабатывает предупреждения с помощью внешней системы.
DeprecationWarning и PendingDeprecationWarning
По умолчанию pytest отображает предупреждения DeprecationWarning и PendingDeprecationWarning, возникающие в пользовательском коде и сторонних библиотеках, как рекомендуется в PEP 565. Это помогает пользователям поддерживать современность кода и избегать сбоев, которые могут возникнуть после окончательного удаления устаревших возможностей.
Однако если пользователь перехватывает в тесте предупреждения любого типа — с помощью pytest.warns(), pytest.deprecated_call() или фикстуры recwarn, — никакие предупреждения отображаться не будут.
Иногда бывает полезно скрыть некоторые предупреждения об устаревании, возникающие в коде, который вы не контролируете (например, в сторонних библиотеках). В этом случае можно использовать параметры фильтрации предупреждений (в конфигурации или метках), чтобы игнорировать такие предупреждения.
Например:
toml
[pytest]
filterwarnings = [
'ignore:.*U.*mode is deprecated:DeprecationWarning',
]
ini
[pytest]
filterwarnings =
ignore:.*U.*mode is deprecated:DeprecationWarning
Это игнорирует все предупреждения типа DeprecationWarning, начало текста которых соответствует регулярному выражению ".*U.*mode is deprecated".
Дополнительные примеры см. в разделах @pytest.mark.filterwarnings и Управление предупреждениями.
Примечание
Если предупреждения настроены на уровне интерпретатора с помощью переменной среды PYTHONWARNINGS или параметра командной строки -W, pytest по умолчанию не будет настраивать фильтры.
Кроме того, pytest не следует рекомендации PEP 565 сбрасывать все фильтры предупреждений, поскольку это может нарушить работу наборов тестов, которые самостоятельно настраивают фильтры, вызывая warnings.simplefilter() (пример см. в #2430).
Проверка того, что код вызывает предупреждение об устаревании
Также можно использовать pytest.deprecated_call(), чтобы проверить, что вызов определенной функции вызывает предупреждение типа DeprecationWarning, PendingDeprecationWarning или FutureWarning:
import pytest
def test_myfunction_deprecated():
with pytest.deprecated_call():
myfunction(17)
Этот тест завершится с ошибкой, если при вызове myfunction с аргументом 17 не будет выдано предупреждение об устаревании.
Проверка предупреждений с помощью функции warns
Проверить, что код выдает определенное предупреждение, можно с помощью pytest.warns(). Она работает подобно raises (за исключением того, что raises перехватывает не все исключения, а только expected_exception):
import warnings
import pytest
def test_warning():
with pytest.warns(UserWarning):
warnings.warn("my warning", UserWarning)
Тест завершится с ошибкой, если соответствующее предупреждение не будет выдано. Используйте именованный аргумент match, чтобы проверить соответствие предупреждения тексту или регулярному выражению. Чтобы сопоставить буквальную строку, которая может содержать специальные символы регулярных выражений, например ( или ., сначала можно экранировать шаблон с помощью re.escape.
Несколько примеров:
>>> with warns(UserWarning, match="must be 0 or None"):
... warnings.warn("value must be 0 or None", UserWarning)
...
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("value must be 42", UserWarning)
...
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
...
Failed: Regex pattern did not match any of the 1 warnings emitted.
Regex: ...
Emitted warnings: ...UserWarning...
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
... warnings.warn("issue with foo() func")
...
Функция также возвращает список всех выданных предупреждений (объектов warnings.WarningMessage), который можно использовать для получения дополнительной информации:
with pytest.warns(RuntimeWarning) as record:
warnings.warn("another warning", RuntimeWarning)
# check that only one warning was raised
assert len(record) == 1
# check that the message matches
assert record[0].message.args[0] == "another warning"
Кроме того, подробно изучить выданные предупреждения можно с помощью фикстуры recwarn (см. ниже).
Фикстура recwarn автоматически сбрасывает фильтр предупреждений в конце теста, поэтому глобальное состояние не сохраняется.
Запись предупреждений
Выданные предупреждения можно записывать с помощью менеджера контекста pytest.warns() или фикстуры recwarn.
Чтобы записывать предупреждения с помощью pytest.warns(), не проверяя их, не указывайте ожидаемый тип предупреждения. По умолчанию будет использоваться общий тип Warning:
with pytest.warns() as record:
warnings.warn("user", UserWarning)
warnings.warn("runtime", RuntimeWarning)
assert len(record) == 2
assert str(record[0].message) == "user"
assert str(record[1].message) == "runtime"
Фикстура recwarn записывает предупреждения на протяжении всей функции:
import warnings
def test_hello(recwarn):
warnings.warn("hello", UserWarning)
assert len(recwarn) == 1
w = recwarn.pop(UserWarning)
assert issubclass(w.category, UserWarning)
assert str(w.message) == "hello"
assert w.filename
assert w.lineno
И фикстура recwarn, и менеджер контекста pytest.warns() возвращают один и тот же интерфейс для записанных предупреждений: экземпляр WarningsRecorder. Чтобы просмотреть записанные предупреждения, можно перебрать этот экземпляр, вызвать для него len, чтобы получить количество записанных предупреждений, или обратиться к нему по индексу, чтобы получить конкретное предупреждение.
Дополнительные варианты использования предупреждений в тестах
Ниже приведены часто встречающиеся в тестах ситуации, связанные с предупреждениями, и рекомендации по их обработке:
- Чтобы проверить, что выдано хотя бы одно из указанных предупреждений, используйте:
def test_warning():
with pytest.warns((RuntimeWarning, UserWarning)):
...
- Чтобы проверить, что выданы только определенные предупреждения, используйте:
def test_warning(recwarn):
...
assert len(recwarn) == 1
user_warning = recwarn.pop(UserWarning)
assert issubclass(user_warning.category, UserWarning)
- Чтобы проверить, что предупреждения не выданы, используйте:
def test_warning():
with warnings.catch_warnings():
warnings.simplefilter("error")
...
- Чтобы подавить предупреждения, используйте:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
...
Пользовательские сообщения об ошибках
Запись предупреждений позволяет создавать пользовательские сообщения об ошибках тестов на случай, если предупреждения не выданы или выполняются другие условия.
def test():
with pytest.warns(Warning) as record:
f()
if not record:
pytest.fail("Expected a warning!")
Если при вызове f предупреждения не выдаются, то not record будет иметь значение True. После этого можно вызвать pytest.fail() с пользовательским сообщением об ошибке.
Внутренние предупреждения pytest
В некоторых ситуациях pytest может выдавать собственные предупреждения, например при неправильном использовании или использовании устаревших возможностей.
Например, pytest выдаст предупреждение, если обнаружит класс, соответствующий python_classes, но также определяющий конструктор __init__, поскольку это не позволяет создать экземпляр класса:
# content of test_pytest_warnings.py
class Test:
def __init__(self):
pass
def test_foo(self):
assert 1 == 1
$ pytest test_pytest_warnings.py -q
============================= warnings summary =============================
test_pytest_warnings.py:1
/home/sweet/project/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py)
class Test:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
1 warning in 0.12s
Эти предупреждения можно фильтровать с помощью тех же встроенных механизмов, которые используются для фильтрации других типов предупреждений.
Ознакомьтесь с нашей политикой обратной совместимости, чтобы узнать, как мы выводим возможности из эксплуатации и впоследствии удаляем их.
Полный список предупреждений приведен в справочной документации.
Предупреждения о ресурсах
Если модуль tracemalloc включен, при перехвате pytest предупреждения ResourceWarning можно получить дополнительные сведения об источнике предупреждения.
Удобный способ включить tracemalloc при запуске тестов — задать переменной среды PYTHONTRACEMALLOC достаточно большое количество кадров (например, 20, однако подходящее значение зависит от приложения).
Дополнительные сведения см. в разделе Режим разработки Python документации Python.
© 2015–2026 Holger Krekel and pytest-dev team
Licensed under the MIT License.
https://docs.pytest.org/en/stable/how-to/capture-warnings.html