【问题标题】:How to skip a pytest using an external fixture?如何使用外部夹具跳过 pytest?
【发布时间】:2015-03-26 13:44:00
【问题描述】:

背景

我在conftest file 中运行py.testfixture。你可以看到下面的代码(这一切都很好):

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"

问题

现在我希望能够跳过一些不适用于我当前测试运行的测试。在我的示例中,我正在为 iOSAndroid 运行测试(这仅用于演示目的,可以是任何其他表达式)。

不幸的是,我无法在skipif 语句中获得(我的外部定义的fixtureplatform_external。当我运行下面的代码时,我收到以下异常: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 文件中定义 fixture 并使用它(有条件地)跳过测试

【问题讨论】:

    标签: python decorator pytest python-decorators


    【解决方案1】:

    在评估skipif 的表达式时,似乎 py.test 不使用测试装置。以您的示例为例,test_ios 实际上是成功的,因为它将模块命名空间中的 function 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' 
    

    希望有帮助!

    【讨论】:

    • 谢谢 - 我昨天也选择了标记作为解决方法,但不喜欢它,因为它没有你的优雅。 (我使用pytest_runtest_setup 进行标记检查)。但是鉴于 py.tests 的限制,这似乎是最接近我的问题的解决方案,我将更新我的问题以使其保持一致。
    • 嗯,收到关于 'skip_platform' not a registered marker 的错误 - 如果我在 conftest 文件之外制作固定装置有关系吗?
    • get_marker 已被删除,现在应该是 get_closest_marker: github.com/pytest-dev/pytest/pull/4564
    【解决方案2】:

    Bruno Oliveira 的解决方案有效,但对于新的 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' 
    

    【讨论】:

      【解决方案3】:

      answer 到另一个 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")
      
      

      【讨论】:

        【解决方案4】:

        我遇到了类似的问题,我不知道这是否仍然与您相关,但我可能已经找到了一种解决方法,可以满足您的需求。

        想法是扩展MarkEvaluator类并重写_getglobals方法,强制在评估器使用的全局集中添加fixture值:

        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' 值已更新。

        希望对你有帮助。

        干杯

        亚历克斯

        【讨论】:

          猜你喜欢
          • 2019-03-22
          • 1970-01-01
          • 1970-01-01
          • 2019-10-20
          • 2022-01-09
          • 2021-06-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多