python 如何使用外部fixture跳过pytest?

polkgigr  于 2022-12-25  发布在  Python
关注(0)|答案(4)|浏览(188)

背景

我在一个conftest文件中运行一个py.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')
    • 对照品py**
import pytest

@pytest.fixture
def platform_external():
    return "android"

问题

现在我希望能够跳过一些不适用于我当前测试运行的测试。在我的示例中,我正在运行iOSAndroid的测试(这仅用于演示目的,可以是任何其他表达式)。
不幸的是,我无法在skipif语句中获取(我的 * external * definedfixtureplatform_external。我不知道这是否是一个py。测试bug,因为 * 本地 * 定义的fixture正在工作。

    • 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')

所以我想我将创建我自己的装饰器,只是为了确保它不会接收fixture作为参数:

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并使用它(有条件地)跳过一个测试

ttvkxqim

ttvkxqim1#

在为skipif计算表达式时,py.test似乎没有使用测试fixture。在您的示例中,test_ios实际上是成功的,因为它将在模块的命名空间中找到的函数platform"ios"字符串进行比较。其评估结果为False,因此测试执行并成功。如果pytest按照您的预期插入fixture进行评估,则应该跳过该测试。
您的问题(不是您的问题)的一个解决方案是实现一个fixture,该fixture检查测试中的标记,并相应地跳过它们:

# 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参数,它会使fixture自动包含在所有测试中,然后您的测试可以标记哪些平台跳过,如下所示:

@pytest.mark.skip_platform('ios')
def test_ios(platform, request):
    assert 0, 'should be skipped'
sh7euo9m

sh7euo9m2#

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",
  )

use:

@pytest.mark.skip_platform('ios')
def test_ios(platform, request):
    assert 0, 'should be skipped'
yeotifhr

yeotifhr3#

从这个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")
vecaoik1

vecaoik14#

我有一个类似的问题,我不知道这是否仍然与您相关,但我可能已经找到了一个变通办法,将做你想要的。
其思想是扩展MarkEvaluator类并覆盖_getglobals方法,以强制在evaluator使用的全局集中添加fixture值:

    • 对照品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

    • 测试示例. 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'值已经更新。
希望有帮助。
干杯
亚历克斯

相关问题