如何使用外部夹具跳过 pytest?
How to skip a pytest using an external fixture?
背景
我正在 运行宁 py.test with a fixture in a conftest file。您可以看到下面的代码(一切正常):
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"
问题
现在我希望能够跳过一些不适用于我当前测试的测试-运行。在我的示例中,我正在 运行ning 测试 iOS 或 Android(这只是为了仅用于演示目的,可以是任何其他表达式)。
不幸的是,我无法在 skipif
语句中找到(我的 外部 定义的 fixture)platform_external
.当我 运行 下面的代码时,我收到以下异常:NameError: name 'platform_external' is not defined
。我不知道这是否是 py.test 错误,因为 locally 定义的固定装置正在工作。
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 并使用它(有条件地)跳过测试?
似乎 py.test 在计算 skipif
的表达式时没有使用测试装置。根据您的示例,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'
希望对您有所帮助!
我遇到了类似的问题,我不知道这是否仍然与您相关,但我可能已经找到了可以满足您要求的解决方法。
想法是扩展 MarkEvaluator
class 并覆盖 _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' 值已更新。
希望对您有所帮助。
干杯
亚历克斯
从这个 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")
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'
背景
我正在 运行宁 py.test with a fixture in a conftest file。您可以看到下面的代码(一切正常):
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"
问题
现在我希望能够跳过一些不适用于我当前测试的测试-运行。在我的示例中,我正在 运行ning 测试 iOS 或 Android(这只是为了仅用于演示目的,可以是任何其他表达式)。
不幸的是,我无法在 skipif
语句中找到(我的 外部 定义的 fixture)platform_external
.当我 运行 下面的代码时,我收到以下异常:NameError: name 'platform_external' is not defined
。我不知道这是否是 py.test 错误,因为 locally 定义的固定装置正在工作。
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 并使用它(有条件地)跳过测试?
似乎 py.test 在计算 skipif
的表达式时没有使用测试装置。根据您的示例,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'
希望对您有所帮助!
我遇到了类似的问题,我不知道这是否仍然与您相关,但我可能已经找到了可以满足您要求的解决方法。
想法是扩展 MarkEvaluator
class 并覆盖 _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' 值已更新。
希望对您有所帮助。
干杯
亚历克斯
从这个 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")
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'