Как пропустить pytest с помощью внешнего прибора?
Фон
Я запускаю py.test с fixture в файл подчета. Вы можете увидеть код ниже (все это работает отлично):
example_test.py
import pytest
@pytest.fixture
def platform():
return "ios"
@pytest.mark.skipif("platform == 'ios'")
def test_ios(platform):
if platform != 'ios':
raise Exception('not ios')
def test_android_external(platform_external):
if platform_external != 'android':
raise Exception('not android')
conftest.py
import pytest
@pytest.fixture
def platform_external():
return "android"
Проблема
Теперь я хочу пропустить некоторые тесты, которые не применяются к текущему тесту. В моем примере я запускаю тесты либо для iOS, либо Android (это только для демонстрационных целей и может быть любым другим выражением).
К сожалению, я не могу получить (мое внешнее определение привязка) platform_external
в инструкции skipif
. Когда я запускаю код ниже, я получаю следующее исключение: NameError: name 'platform_external' is not defined
. Я не знаю, является ли это ошибкой py.test, поскольку локально определенные приборы работают.
для example_test.py
@pytest.mark.skipif("platform_external == 'android'")
def test_android(platform_external):
"""This test will fail as 'platform_external' is not available in the decorator.
It is only available for the function parameter."""
if platform_external != 'android':
raise Exception('not android')
Итак, я подумал, что просто создаю свой собственный декоратор, просто чтобы увидеть, что он не получит светильники в качестве параметров:
from functools import wraps
def platform_custom_decorator(func):
@wraps(func)
def func_wrapper(*args, **kwargs):
return func(*args, **kwargs)
return func_wrapper
@platform_custom_decorator
def test_android_2(platform_external):
"""This test will also fail as 'platform_external' will not be given to the
decorator."""
if platform_external != 'android':
raise Exception('not android')
Вопрос
Как я могу определить привязку в файле conftest и использовать его (условно) пропустить тест?
Ответы
Ответ 1
Кажется, что py.test не использует тестовые данные при оценке выражения для skipif
. В вашем примере test_ios
на самом деле успешен, потому что он сравнивает platform
функций, найденную в пространстве имен модуля, со строкой "ios"
, которая оценивается как False
следовательно, тест выполняется и завершается успешно. Если pytest вставлял прибор для оценки, как вы ожидаете, этот тест следует пропустить.
Решением вашей проблемы (но не вашего вопроса) было бы внедрение приспособления, которое проверяет метки в тестах и пропускает их соответствующим образом:
# conftest.py
import pytest
@pytest.fixture
def platform():
return "ios"
@pytest.fixture(autouse=True)
def skip_by_platform(request, platform):
if request.node.get_closest_marker('skip_platform'):
if request.node.get_closest_marker('skip_platform').args[0] == platform:
pytest.skip('skipped on this platform: {}'.format(platform))
Ключевым моментом является параметр autouse
, который делает этот прибор автоматически включенным во все тесты. Тогда ваши тесты могут пометить, какие платформы пропустить следующим образом:
@pytest.mark.skip_platform('ios')
def test_ios(platform, request):
assert 0, 'should be skipped'
Надеюсь, это поможет!
Ответ 2
Используя вдохновение из этого ответа на другой вопрос SO, я использую этот подход к этой проблеме, который хорошо работает:
import pytest
@pytest.fixture(scope='session')
def requires_something(request):
something = 'a_thing'
if request.param != something:
pytest.skip(f"Test requires {request.param} but environment has {something}")
@pytest.mark.parametrize('requires_something',('something_else',), indirect=True)
def test_indirect(requires_something):
print("Executing test: test_indirect")
Ответ 3
У меня была аналогичная проблема, и я не знаю, действительно ли это для вас актуально, но я, возможно, нашел обходное решение, которое будет делать то, что вы хотите.
Идея состоит в том, чтобы расширить класс MarkEvaluator
и переопределить метод _getglobals
, чтобы принудительно добавить значения привязки в глобальный набор, используемый оценщиком:
conftest.py
from _pytest.skipping import MarkEvaluator
class ExtendedMarkEvaluator(MarkEvaluator):
def _getglobals(self):
d = super()._getglobals()
d.update(self.item._request._fixture_values)
return d
добавьте крюк для проверки вызовов:
def pytest_runtest_call(item):
evalskipif = ExtendedMarkEvaluator(item, "skipif_call")
if evalskipif.istrue():
pytest.skip('[CANNOT RUN]' + evalskipif.getexplanation())
тогда вы можете использовать маркер skipif_call
в своем тестовом примере:
test_example.py
class Machine():
def __init__(self, state):
self.state = state
@pytest.fixture
def myfixture(request):
return Machine("running")
@pytest.mark.skipif_call('myfixture.state != "running"')
def test_my_fixture_running_success(myfixture):
print(myfixture.state)
myfixture.state = "stopped"
assert True
@pytest.mark.skipif_call('myfixture.state != "running"')
def test_my_fixture_running_fail(myfixture):
print(myfixture.state)
assert False
@pytest.mark.skipif_call('myfixture.state != "stopped"')
def test_my_fixture_stopped_success(myfixture):
print(myfixture.state)
myfixture.state = "running"
@pytest.mark.skipif_call('myfixture.state != "stopped"')
def test_my_fixture_stopped_fail(myfixture):
print(myfixture.state)
assert False
Run
pytest -v --tb=line
============================= test session starts =============================
[...]
collected 4 items
test_example.py::test_my_fixture_running_success PASSED
test_example.py::test_my_fixture_running_fail FAILED
test_example.py::test_my_fixture_stopped_success PASSED
test_example.py::test_my_fixture_stopped_fail FAILED
================================== FAILURES ===================================
C:\test_example.py:21: assert False
C:\test_example.py:31: assert False
===================== 2 failed, 2 passed in 0.16 seconds ======================
Проблема
К сожалению, это работает только один раз для каждого оценочного выражения, поскольку MarkEvaluator использует кешированный eval, основанный на выражении как ключ, поэтому в следующий раз, когда будет проверено одно и то же выражение, результатом будет кешированное значение.
Решение
Выражение оценивается по методу _istrue
. К сожалению, нет способа настроить оценщика для исключения результатов кеширования.
Единственный способ избежать кеширования - переопределить метод _istrue
, чтобы не использовать функцию cached_eval:
class ExtendedMarkEvaluator(MarkEvaluator):
def _getglobals(self):
d = super()._getglobals()
d.update(self.item._request._fixture_values)
return d
def _istrue(self):
if self.holder:
self.result = False
args = self.holder.args
kwargs = self.holder.kwargs
for expr in args:
import _pytest._code
self.expr = expr
d = self._getglobals()
# Non cached eval to reload fixture values
exprcode = _pytest._code.compile(expr, mode="eval")
result = eval(exprcode, d)
if result:
self.result = True
self.reason = expr
self.expr = expr
break
return self.result
return False
Run
pytest -v --tb=line
============================= test session starts =============================
[...]
collected 4 items
test_example.py::test_my_fixture_running_success PASSED
test_example.py::test_my_fixture_running_fail SKIPPED
test_example.py::test_my_fixture_stopped_success PASSED
test_example.py::test_my_fixture_stopped_fail SKIPPED
===================== 2 passed, 2 skipped in 0.10 seconds =====================
Теперь тесты пропускаются, поскольку значение "myfixture" обновлено.
Надеюсь, что это поможет.
Приветствия
Алекс
Ответ 4
Решение от Bruno Oliveira работает, но для нового pytest вам нужно добавить строку pytest_configure:
# conftest.py
import pytest
@pytest.fixture
def platform():
return "ios"
@pytest.fixture(autouse=True)
def skip_by_platform(request, platform):
if request.node.get_closest_marker('skip_platform'):
if request.node.get_closest_marker('skip_platform').args[0] == platform:
pytest.skip('skipped on this platform: {}'.format(platform))
def pytest_configure(config):
config.addinivalue_line(
"markers", "skip_by_platform(platform): skip test for the given search engine",
)
Использование:
@pytest.mark.skip_platform('ios')
def test_ios(platform, request):
assert 0, 'should be skipped'