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

Начиная с версии 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-7.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 warning filter и -W option, pytest предоставляет свой собственный флаг -W для управления тем, какие предупреждения игнорируются, отображаются или превращаются в ошибки. См. документацию warning filter для более сложных случаев использования.

Этот пример кода показывает, как рассматривать любой класс предупреждения категории 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

Этот же параметр можно установить в файле pytest.ini или pyproject.toml с помощью параметра ini filterwarnings. Например, приведенная ниже конфигурация будет игнорировать все предупреждения пользователя и специфические предупреждения об износе, соответствующие регексу, но будет преобразовывать все остальные предупреждения в ошибки.

# pytest.ini
[pytest]
filterwarnings =
    error
    ignore::UserWarning
    ignore:function ham\(\) is deprecated:DeprecationWarning
# pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
    "error",
    "ignore::UserWarning",
    # note the use of single quote below to denote "raw" strings in TOML
    'ignore:function ham\(\) is deprecated:DeprecationWarning',
]

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

@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

Фильтры, применяемые с помощью метки, имеют приоритет над фильтрами, переданными в командной строке или настроенными с помощью опции filterwarnings ini>.

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

# turns all warnings into errors for this module
pytestmark = pytest.mark.filterwarnings("error")

Признательность Флориану Шульце за эталонную реализацию в pytest-warnings плагине..

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

Хотя это не рекомендуется, вы можете использовать опцию командной строки --disable-warnings, чтобы полностью исключить сводку предупреждений из вывода результатов выполнения теста.

Полное отключение перехвата предупреждений

Этот плагин включен по умолчанию, но может быть полностью отключен в вашем файле pytest.ini с помощью:

[pytest]
addopts = -p no:warnings

Или передать -p no:warnings в командной строке. Это может быть полезно, если ваш тестовый набор обрабатывает предупреждения с помощью внешней системы.

DeprecationWarning и PendingDeprecationWarning

По умолчанию pytest будет отображать предупреждения DeprecationWarning и PendingDeprecationWarning из пользовательского кода и сторонних библиотек, как рекомендовано PEP 565. Это помогает пользователям поддерживать код в современном состоянии и избегать поломок, когда устаревшие предупреждения фактически удаляются.

Однако в конкретном случае, когда пользователи фиксируют любой тип предупреждений в своем тесте, либо с помощью pytest.warns(), pytest.deprecated_call(), либо используя приспособление recwarn, предупреждение не будет отображаться вообще.

Иногда полезно скрыть некоторые специфические предупреждения об износе, возникающие в коде, который вы не можете контролировать (например, в библиотеках сторонних разработчиков), в этом случае вы можете использовать опции фильтров предупреждений (ini или marks), чтобы игнорировать эти предупреждения.

Например:

[pytest]
filterwarnings =
    ignore:.*U.*mode is deprecated:DeprecationWarning

При этом будут игнорироваться все предупреждения типа DeprecationWarning, в которых начало сообщения соответствует регулярному выражению ".*U.*mode is deprecated".

Дополнительные примеры смотрите в @pytest.mark.filterwarnings и Controlling warnings.

Примечание

Если предупреждения настроены на уровне интерпретатора с помощью переменной окружения PYTHONWARNINGS или опции командной строки -W, pytest по умолчанию не будет настраивать никаких фильтров.

Также pytest не следует предложению PEP 506 сбросить все фильтры предупреждений, поскольку это может нарушить работу тестовых наборов, которые сами настраивают фильтры предупреждений, вызывая warnings.simplefilter() (см. пример issue #2430).

Обеспечение срабатывания предупреждения об устаревании кода

Вы также можете использовать pytest.deprecated_call() для проверки того, что определенный вызов функции вызывает DeprecationWarning или PendingDeprecationWarning:

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 для утверждения, что предупреждение соответствует тексту или regex:

>>> 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: DID NOT WARN. No warnings of type ...UserWarning... were emitted...

Вы также можете вызвать pytest.warns() на функцию или строку кода:

pytest.warns(expected_warning, func, *args, **kwargs)
pytest.warns(expected_warning, "func(*args, **kwargs)")

Функция также возвращает список всех выданных предупреждений (в виде объектов 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, чтобы получить количество записанных предупреждений, или проиндексировать его, чтобы получить конкретное записанное предупреждение.

Полный API: WarningsRecorder.

Дополнительные случаи использования предупреждений в тестах

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

  • Чтобы убедиться в том, что будет выдано ** хотя бы одно** из указанных предупреждений, используйте:

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

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

Пожалуйста, ознакомьтесь с нашим Политика обратной совместимости, чтобы узнать, как мы действуем в отношении устаревания и, в конечном счете, удаления функций.

Полный список предупреждений приведен в the reference documentation.

Предупреждения о ресурсах

Дополнительная информация об источнике ResourceWarning может быть получена при перехвате pytest, если включен модуль tracemalloc.

Один из удобных способов включить tracemalloc при выполнении тестов - установить PYTHONTRACEMALLOC на достаточно большое количество кадров (скажем, 20, но это число зависит от приложения).

Для получения дополнительной информации обратитесь к разделу Python Development Mode в документации по Python.

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