Сообщить об ошибке.

Управление предупреждениями warnings в pytest

Фреймворк pytest автоматически ловит предупреждения во время выполнения теста и отображает их в конце сеанса.

Содержание:


Управление предупреждениями в pytest.

Для управления предупреждениями при тестировании, какие нужно игнорировать, какие отображать в конце сеанса, а какие преобразовывать в ошибки, фреймворк pytest использует опцию CLI -W. Эта опция работает аналогично флагу -W интерпретатора Python. Более подробные варианты использования смотрите в материале "Фильтр предупреждений warnings".

Рассмотрим следующий тест:

# test_warnings.py
import warnings

def api_v1():
    # определяем пользовательское предупреждение
    warnings.warn(UserWarning("api v1, необходимы функции из v2"))
    return 1

def test_one():
    assert api_v1() == 1

Теперь, что бы pytest рассматривал категорию предупреждений UserWarning как ошибку необходимо запустить тест со следующими опциями:

$ pytest -q test_warnings.py -W error::UserWarning

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

# файл pytest.ini
[pytest]
filterwarnings =
    error
    ignore::UserWarning
    ignore:function ham\(\) is deprecated:DeprecationWarning
# файл pyproject.toml
[tool.pytest.ini_options]
filterwarnings = [
    "error",
    "ignore::UserWarning",
    # обратите внимание на использование одинарной кавычки
    # для обозначения "сырых" строк в TOML.
    'ignore:function ham\(\) is deprecated:DeprecationWarning',
]

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

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

Использование декоратора @pytest.mark.filterwarnings().

Чтобы добавить фильтры предупреждений к определенным элементам теста, можно использовать декоратор @pytest.mark.filterwarnings(). Этот декоратор позволит более точно контролировать, какие предупреждения должны фиксироваться на уровне теста, класса или даже модуля:

import warnings

def api_v1():
    warnings.warn(UserWarning("api v1, необходимы функции из v2"))
    return 1

@pytest.mark.filterwarnings("ignore:api v1")
def test_one():
    assert api_v1() == 1

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

Можно применить фильтр ко всем тестам класса, используя @pytest.mark.filterwarnings в качестве декоратора класса, или ко всем тестам в модуле, установив переменную/атрибут модуля pytestmark:

# переведет все предупреждения в ошибки для этого модуля
pytestmark = pytest.mark.filterwarnings("error")

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

Плагин захвата предупреждений включен в pytest по умолчанию, но его можно полностью отключить в файле pytest.ini с помощью:

# pytest.ini
[pytest]
addopts = -p no:warnings

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

Предупреждения DeprecationWarning и PendingDeprecationWarning.

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

Иногда полезно скрыть некоторые конкретные предупреждения об устаревании в коде, который невозможно контролировать (например, в сторонних библиотеках). Чтобы игнорировать эти предупреждения можно использовать pytest.ini или @pytest.mark.filterwarnings().

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

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

Примечание:

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

Кроме того, pytest не сбрасывает все фильтры предупреждений, т.к. это может нарушить тесты, которые сами настраивают фильтры предупреждений, вызвав warnings.simplefilter().

Функция pytest.deprecated_call().

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

import pytest

def test_myfunction_deprecated():
    with pytest.deprecated_call():
        myfunction(17)

Этот тест завершится ошибкой, если myfunction() не выдаст предупреждение об устаревании при вызове с аргументом 17.

Функция pytest.warns().

Используя функцию pytest.warns(expected_warning, match=None) можно проверить, вызывает ли код конкретное предупреждение. Функция pytest.warns() работает так же, как и pytest.raises():

  • Аргумент expected_warning: объект исключения Warning или кортеж объектов Warning;
  • Аргумент match: если указано, то это строка, содержащая регулярное выражение или объект регулярного выражения, который проверяется на соответствие строковому представлению исключения с помощью re.search(). Может содержать специальные символы, шаблон может быть сначала экранирован с помощью re.escape().

    Аргумент match используется только тогда, когда pytest.warns() используется в качестве диспетчера контекста. При использовании pytest.warns() в качестве функции можно использовать: pytest.warns(Warn, func, match="passed on").match("my pattern")

import warnings
import pytest

def test_warning():
    with pytest.warns(UserWarning):
        warnings.warn("my warning", UserWarning)

Если соответствующее предупреждение не возникнет, то тест завершится неудачей. Функция pytest.warns() имеет ключевой аргумент match, который принимает регулярное выражение для проверки соответствия тексту предупреждения:

>>> with warns(UserWarning, match=r'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)

# проверка на выдачу только одного предупреждения
assert len(record) == 1
# проверка на соответствие сообщению 
assert record[0].message.args[0] == "another warning"

Кроме того, можно подробно изучить возникающие предупреждения, используя встроенную фикстуру recwarn.

Встроенная фикстура recwarn автоматически обеспечивает сброс фильтра предупреждений в конце теста, поэтому утечка глобального состояния не происходит.

Сохранение предупреждений (фикстура recwarn).

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

Сохранение поднятых предупреждений с помощью pytest.warns():

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() или получить конкретное записанное предупреждение по индексу.

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

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

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

    with pytest.warns():
      ...
    
  • Чтобы узнать, что предупреждения не выдаются, используйте:

    with warnings.catch_warnings():
      warnings.simplefilter("error")
      ...
    
  • Для отключения предупреждений, используйте:

    with warnings.catch_warnings():
      warnings.simplefilter("ignore")
    

Внутренние предупреждения pytest.

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

Например, pytest выдаст предупреждение, если встретит класс, соответствующий python_classes, но также определяющий конструктор __init__, т.к. это предотвращает создание экземпляра класса:

# test_pytest_warnings.py
class Test:
    def __init__(self):
        pass

    def test_foo(self):
        assert 1 == 1

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