Как использовать skip и xfail для работы с тестами, которые не могут быть успешными

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

skip означает, что вы ожидаете, что ваш тест пройдет только при выполнении некоторых условий, в противном случае pytest должен вообще пропустить тест. Обычными примерами являются пропуск тестов только для windows на платформах, отличных от Windows, или пропуск тестов, которые зависят от внешнего ресурса, недоступного в данный момент (например, базы данных).

xfail означает, что вы ожидаете, что тест по какой-то причине не пройдет. Обычный пример - тест на еще не реализованную функцию или еще не исправленную ошибку. Если тест проходит, несмотря на то, что ожидалось, что он не пройдет (отмечено pytest.mark.xfail), это xpass, и о нем будет сообщено в сводке тестов.

pytest подсчитывает и перечисляет skip и xfail тесты отдельно. Подробная информация о пропущенных/xfailed тестах не показывается по умолчанию, чтобы не загромождать вывод. Вы можете использовать опцию -r, чтобы увидеть подробности, соответствующие «коротким» буквам, показанным в прогрессе теста:

pytest -rxXs  # show extra info on xfailed, xpassed, and skipped tests

Более подробную информацию об опции -r можно получить, выполнив pytest -h.

(См. Опции встроенного файла конфигурации)

Пропуск тестовых функций

Самый простой способ пропустить тестовую функцию - пометить ее декоратором skip, которому можно передать необязательный reason:

@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
    ...

Также можно пропустить императив во время выполнения или настройки теста, вызвав функцию pytest.skip(reason):

def test_function():
    if not valid_config():
        pytest.skip("unsupported configuration")

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

Также можно пропустить весь модуль, используя pytest.skip(reason, allow_module_level=True) на уровне модуля:

import sys

import pytest

if not sys.platform.startswith("win"):
    pytest.skip("skipping windows-only tests", allow_module_level=True)

Ссылка: pytest.mark.skip

skipif

Если вы хотите пропустить что-то условно, то вместо этого можно использовать skipif. Вот пример пометки тестовой функции для пропуска при запуске на интерпретаторе, более раннем, чем Python3.10:

import sys


@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function():
    ...

Если во время сбора условие оценивается как True, тестовая функция будет пропущена, а при использовании -rs в сводке появится указанная причина.

Вы можете совместно использовать маркеры skipif между модулями. Рассмотрим этот тестовый модуль:

# content of test_mymodule.py
import mymodule

minversion = pytest.mark.skipif(
    mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
)


@minversion
def test_function():
    ...

Вы можете импортировать маркер и повторно использовать его в другом тестовом модуле:

# test_myothermodule.py
from test_mymodule import minversion


@minversion
def test_anotherfunction():
    ...

Для больших тестовых наборов обычно целесообразно иметь один файл, в котором определяются маркеры, которые затем последовательно применяются во всем тестовом наборе.

В качестве альтернативы вы можете использовать condition strings вместо булевых значений, но они не могут быть легко разделены между модулями, поэтому поддерживаются в основном из соображений обратной совместимости.

Ссылка: pytest.mark.skipif

Пропустить все тестовые функции класса или модуля

Вы можете использовать маркер skipif (как и любой другой маркер) в классах:

@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
class TestPosixCalls:
    def test_function(self):
        "will not be setup or run under 'win32' platform"

Если условие равно True, этот маркер выдаст результат пропуска для каждого из тестовых методов данного класса.

Если вы хотите пропустить все тестовые функции модуля, вы можете использовать глобал pytestmark:

# test_module.py
pytestmark = pytest.mark.skipif(...)

Если к тестовой функции применяется несколько декораторов skipif, она будет пропущена, если любое из условий пропуска истинно.

Пропуск файлов или каталогов

Иногда вам может понадобиться пропустить целый файл или каталог, например, если тесты полагаются на специфические особенности версии Python или содержат код, который вы не хотите, чтобы pytest выполнял. В этом случае вы должны исключить файлы и каталоги из коллекции. Для получения дополнительной информации обратитесь к разделу Настройка сбора тестов.

Пропуск отсутствующей зависимости импорта

Вы можете пропустить тесты на отсутствующий импорт, используя pytest.importorskip на уровне модуля, внутри теста или функции настройки теста.

docutils = pytest.importorskip("docutils")

Если docutils не может быть импортирован сюда, это приведет к пропуску теста. Вы также можете пропустить тест, основываясь на номере версии библиотеки:

docutils = pytest.importorskip("docutils", minversion="0.3")

Версия будет считана из атрибута __version__ указанного модуля.

Резюме

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

  1. Безусловный пропуск всех тестов в модуле:

pytestmark = pytest.mark.skip("all tests still WIP")
  1. Пропустить все тесты в модуле на основании некоторого условия:

pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="tests for linux only")
  1. Пропустить все тесты в модуле, если какой-то импорт отсутствует:

pexpect = pytest.importorskip("pexpect")

XFail: пометить функции теста как ожидаемые для отказа

Вы можете использовать маркер xfail, чтобы указать, что вы ожидаете неудачу теста:

@pytest.mark.xfail
def test_function():
    ...

Этот тест будет запущен, но при неудаче не будет сообщено об отслеженном результате. Вместо этого в терминальных отчетах он будет указан в разделах «ожидается неудача» (XFAIL) или «неожиданно пройден» (XPASS).

Кроме того, можно пометить тест как XFAIL из самого теста или его функции настройки в обязательном порядке:

def test_function():
    if not valid_config():
        pytest.xfail("failing configuration (but should work)")
def test_function2():
    import slow_module

    if slow_module.slow_function():
        pytest.xfail("slow_module taking too long")

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

Это сделает test_function XFAIL. Обратите внимание, что после вызова pytest.xfail() не выполняется никакой другой код, отличный от маркера. Это потому, что он реализован внутренне путем поднятия известного исключения.

Ссылка: pytest.mark.xfail

condition параметр

Если ожидается, что тест не сработает только при определенном условии, вы можете передать это условие в качестве первого параметра:

@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function():
    ...

Обратите внимание, что вы должны передать также и причину (см. описание параметров в pytest.mark.xfail).

reason параметр

Вы можете указать мотив ожидаемого сбоя с помощью параметра reason:

@pytest.mark.xfail(reason="known parser issue")
def test_function():
    ...

raises параметр

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

@pytest.mark.xfail(raises=RuntimeError)
def test_function():
    ...

Тогда тест будет зарегистрирован как обычный сбой, если он не прошел с исключением, не упомянутым в raises.

run параметр

Если тест должен быть помечен как xfail и сообщен как таковой, но даже не должен выполняться, используйте параметр run как False:

@pytest.mark.xfail(run=False)
def test_function():
    ...

Это особенно полезно для тестов xfailing, которые приводят к краху интерпретатора и должны быть исследованы позже.

strict параметр

По умолчанию и XFAIL, и XPASS не проваливают набор тестов. Вы можете изменить это, установив параметр strict «только ключевое слово» в значение True:

@pytest.mark.xfail(strict=True)
def test_function():
    ...

Это приведет к тому, что результаты XPASS («неожиданно пройден») этого теста не пройдут набор тестов.

Вы можете изменить значение параметра по умолчанию strict с помощью параметра xfail_strict ini:

[pytest]
xfail_strict=true

Игнорирование xfail

Указывая в командной строке:

pytest --runxfail

вы можете принудительно запустить и сообщить о тесте, помеченном xfail, как если бы он вообще не был помечен. Это также приводит к тому, что pytest.xfail() не производит никакого эффекта.

Примеры

Вот простой тестовый файл с несколькими вариантами использования:

import pytest

xfail = pytest.mark.xfail


@xfail
def test_hello():
    assert 0


@xfail(run=False)
def test_hello2():
    assert 0


@xfail("hasattr(os, 'sep')")
def test_hello3():
    assert 0


@xfail(reason="bug 110")
def test_hello4():
    assert 0


@xfail('pytest.__version__[0] != "17"')
def test_hello5():
    assert 0


def test_hello6():
    pytest.xfail("reason")


@xfail(raises=IndexError)
def test_hello7():
    x = []
    x[1] = 1

Запуск с опцией report-on-xfail дает следующий результат:

! pytest -rx xfail_demo.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-1.x.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR/example
collected 7 items

xfail_demo.py xxxxxxx                                                [100%]

========================= short test summary info ==========================
XFAIL xfail_demo.py::test_hello
XFAIL xfail_demo.py::test_hello2
  reason: [NOTRUN]
XFAIL xfail_demo.py::test_hello3
  condition: hasattr(os, 'sep')
XFAIL xfail_demo.py::test_hello4
  bug 110
XFAIL xfail_demo.py::test_hello5
  condition: pytest.__version__[0] != "17"
XFAIL xfail_demo.py::test_hello6
  reason: reason
XFAIL xfail_demo.py::test_hello7
============================ 7 xfailed in 0.12s ============================

Пропуск/xfail с параметризацией

При использовании параметризации можно применять маркеры типа skip и xfail к отдельным экземплярам теста:

import sys

import pytest


@pytest.mark.parametrize(
    ("n", "expected"),
    [
        (1, 2),
        pytest.param(1, 0, marks=pytest.mark.xfail),
        pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")),
        (2, 3),
        (3, 4),
        (4, 5),
        pytest.param(
            10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k")
        ),
    ],
)
def test_increment(n, expected):
    assert n + 1 == expected
Вернуться на верх