【问题标题】:How can I rewrite this fixture call so it won't be called directly?我怎样才能重写这个夹具调用,所以它不会被直接调用?
【发布时间】:2020-12-12 11:42:01
【问题描述】:

我在测试文件中定义了以下夹具:

import os
from dotenv import load_dotenv, find_dotenv
from packaging import version # for comparing version numbers

load_dotenv(find_dotenv())
VERSION = os.environ.get("VERSION")
API_URL = os.environ.get("API_URL")

@pytest.fixture()
def skip_before_version():
    """
    Creates a fixture that takes parameters
    skips a test if it depends on certain features implemented in a certain version

    :parameter target_version:
    :parameter type: string
    """
    def _skip_before(target_version):
        less_than = version.parse(current_version) < version.parse(VERSION) 
        return pytest.mark.skipif(less_than)
    return _skip_before


skip_before = skip_before_version()("0.0.1")

我想在某些测试中使用skip_before 作为夹具。我这样称呼它:

#@skip_before_version("0.0.1")     # tried this before and got the same error, so tried reworking it...
@when(parsers.cfparse("{categories} are added as categories"))
def add_categories(skip_before, create_tree, categories):   # now putting the fixture alongside parameters
    pass

当我运行它时,我收到以下错误:

Fixture "skip_before_version" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.
See https://docs.pytest.org/en/stable/fixture.html for more information about fixtures, and
https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code.

这还怎么被直接调用?我该如何解决这个问题?

【问题讨论】:

  • 这还怎么被直接调用? - 因为skip_before = skip_before_version("0.0.1") 行。 我该如何解决这个问题? - 从代码中您想要实现的目标不是很清楚,而且代码看起来已损坏。例如。 skip_before_version 不希望有任何参数,但您将“0.0.1”传递给它。
  • @hoefling 你说得对,我应该在将参数传递给它返回的函数之前调用skip_before_version,即skip_before = skip_before_version()("0.0.1")
  • @hoefling 我仍然不确定如何使用这个夹具,如果版本不够高,它将跳过测试(这意味着它需要将版本号作为参数)。我不能将此夹具作为参数添加到函数中,因为它的语法无效,也不能直接调用它。

标签: python pytest decorator fixtures


【解决方案1】:

如果我正确理解您的目标,您希望能够跳过基于版本限制说明符的测试。有很多方法可以做到这一点;我可以建议一个 autouse 夹具,它将根据自定义标记条件跳过测试。示例:

import os
import pytest
from packaging.specifiers import SpecifierSet


VERSION = "1.2.3"  # read from environment etc.


@pytest.fixture(autouse=True)
def skip_based_on_version_compat(request):
    # get the version_compat marker
    version_compat = request.node.get_closest_marker("version_compat")
    if version_compat is None:  # test is not marked
        return

    if not version_compat.args:  # no specifier passed to marker
        return

    spec_arg = version_compat.args[0]
    spec = SpecifierSet(spec_arg)

    if VERSION not in spec:
        pytest.skip(f"Current version {VERSION} doesn't match test specifiers {spec_arg!r}.")

fixture skip_based_on_version_compat 将在每个测试中被调用,但只有在测试标记为@pytest.mark.version_compat 时才会执行某些操作。示例测试:

@pytest.mark.version_compat(">=1.0.0")
def test_current_gen():
    assert True


@pytest.mark.version_compat(">=2.0.0")
def test_next_gen():
    raise NotImplementedError()

使用VERSION = "1.2.3",将执行第一个测试,跳过第二个测试。请注意调用pytest.skip 以立即跳过测试。在夹具中返回 pytest.mark.skip 不会给您带来任何好处,因为标记早在此之前就已经被评估了。

另外,我注意到您正在编写小黄瓜测试(大概使用pytest-bdd)。使用上述方法,也应该可以跳过整个场景:

@pytest.mark.version_compat(">=1.0.0")
@scenario("my.feature", "my scenario")
def test_scenario():
    pass

或者,您可以在功能文件中标记场景:

Feature: Foo
    Lorem ipsum dolor sit amet.

    @version_compat(">=1.0.0")
    Scenario: doing future stuff
        Given foo is implemented
        When I see foo
        Then I do bar

并使用pytest-bdd-own hooks:

def pytest_bdd_apply_tag(tag, function):
    matcher = re.match(r'^version_compat\("(?P<spec_arg>.*)"\)$', tag)
    spec_arg = matcher.groupdict()["spec_arg"]
    spec = SpecifierSet(spec_arg)
    if VERSION not in spec:
        marker = pytest.mark.skip(
            reason=f"Current version {VERSION} doesn't match restriction {spec_arg!r}."
        )
        marker(function)
        return True

不幸的是,自定义固定装置和标记都不适用于单步跳过(而且您仍然会跳过整个场景,因为它是小黄瓜中的原子测试单元)。我没有找到一种可靠的方法来与 pytest 的东西交朋友 pytest-bdd 步骤;看起来他们只是被忽略了。不过,您可以轻松编写自定义装饰器来实现相同目的:

import functools


def version_compat(spec_arg):

    def deco(func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            spec = SpecifierSet(spec_arg)
            if VERSION not in spec:
                pytest.skip(f"Current version {VERSION} doesn't match test specifiers {spec_arg!r}.")
            return func(*args, **kwargs)

        return wrapper

    return deco

在一个步骤中使用version_compat deco:

@when('I am foo')
@version_compat(">=2.0.0")
def i_am_foo():
    ...

注意排序 - 将装饰器放在 pytest-bdd 自己的东西之外不会触发它们(我想值得打开一个问题,但是嗯)。

【讨论】:

    猜你喜欢
    • 2016-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多