Как я могу переписать этот вызов fixture, чтобы он не вызывался напрямую?

#python #pytest #decorator #fixtures

#python #pytest #декоратор #приспособления

Вопрос:

Я определил следующее приспособление в тестовом файле:

 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.
 

Как это все еще вызывается напрямую? Как я могу это исправить?

Комментарии:

1. Как это все еще вызывается напрямую? — из-за skip_before = skip_before_version("0.0.1") строки. Как я могу это исправить? — из кода не совсем ясно, чего вы пытаетесь достичь, и код выглядит сломанным. Например. skip_before_version не ожидает никаких аргументов, но вы передаете ему «0.0.1».

2. @hoefling Вы правы, я должен позвонить skip_before_version , прежде чем передавать aruments функции, которую он возвращает, т.Е. skip_before = skip_before_version()("0.0.1") .

3. @hoefling Я все еще не уверен, как использовать этот инструмент, который пропустит тест, если версия недостаточно высока (это означает, что он должен принимать номер версии в качестве аргумента). Я не могу добавить этот параметр в качестве параметра в функцию, поскольку это недопустимый синтаксис, и я не могу вызвать его напрямую.

Ответ №1:

Если я правильно понимаю вашу цель, вы хотите иметь возможность пропускать тесты на основе спецификатора ограничения версии. Есть много способов сделать это; Я могу предложить автоматическое устройство, которое пропустит тест на основе пользовательского условия маркера. Пример:

 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}.")
 

Прибор 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 в fixture ничего вам не принесет, поскольку маркеры уже были оценены задолго до этого.

Кроме того, я заметил, что вы пишете тесты gherkin (используя 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 собственные хуки:

 def pytest_bdd_apply_tag(tag, function):
    matcher = re.match(r'^version_compat("(?P<spec_arg>.*)")

К сожалению, ни пользовательские приспособления, ни маркеры не будут работать с пропуском за один шаг (и вы все равно будете пропускать весь сценарий, поскольку это атомарный тестовый модуль в gherkin). Я не нашел надежного способа подружить  pytest-bdd  шаги с материалами pytest; похоже, они просто игнорируются. Тем не менее, вы можете легко написать пользовательский декоратор, служащий той же цели:

 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 собственного материала не вызовет их (я думаю, стоит открыть проблему, но meh).


, 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
К сожалению, ни пользовательские приспособления, ни маркеры не будут работать с пропуском за один шаг (и вы все равно будете пропускать весь сценарий, поскольку это атомарный тестовый модуль в gherkin). Я не нашел надежного способа подружить pytest-bdd шаги с материалами pytest; похоже, они просто игнорируются. Тем не менее, вы можете легко написать пользовательский декоратор, служащий той же цели:


Использование version_compat deco на шаге:


Обратите внимание на порядок — размещение декораторов за пределами pytest-bdd собственного материала не вызовет их (я думаю, стоит открыть проблему, но meh).