Как пропустить pytest с использованием внешнего устройства?

Фон

Я запускаю py.test с fixture в файле conftest. Вы можете увидеть код ниже (все работает нормально):

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 файле и использовать его, чтобы (условно) пропустить тест?


person Marco Pashkov    schedule 27.01.2015    source источник


Ответы (4)


Кажется, 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' 

Надеюсь, это поможет!

person Bruno Oliveira    schedule 28.01.2015
comment
Спасибо - вчера я тоже выбрал маркер в качестве обходного пути, но он мне не понравился, так как он был не таким элегантным, как ваш. (Я использовал pytest_runtest_setup для проверки маркера). Но с учетом ограничений py.tests это кажется ближайшим решением моего вопроса, и я обновлю свой вопрос, чтобы согласовать его. - person Marco Pashkov; 28.01.2015
comment
Хм, возникают ошибки по поводу 'skip_platform' not a registered marker - будет ли иметь значение, если я сделаю приспособление вне файла conftest? - person dwanderson; 23.03.2018
comment
get_marker был удален и теперь должен быть get_closest_marker: github.com/pytest-dev/pytest/pull / 4564 - person Gustavo Bezerra; 07.02.2019

Решение от Бруно Оливейры работает, но для нового pytest (> = 3.5.0) вам нужно добавить 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' 
person user1921483    schedule 18.07.2019

Вдохновленный этим ответом на другой вопрос 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")

person Gregory Kuhn    schedule 26.02.2019

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

Идея состоит в том, чтобы расширить класс 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

Выполнить

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

Выполнить

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 было обновлено.

Надеюсь, это поможет.

Ваше здоровье

Алекс

person Alexper    schedule 01.12.2017