Передайте параметр в функцию фиксации

114

Я использую py.test для тестирования некоторого кода DLL, заключенного в класс MyTester python. Для проверки мне нужно записать некоторые тестовые данные во время тестов и после этого выполнить дополнительную обработку. Поскольку у меня много тестовых _... файлов, я хочу повторно использовать создание объекта тестера (экземпляр MyTester) для большинства моих тестов.

Поскольку объект тестера - это тот, который получил ссылки на переменные и функции DLL, мне нужно передать список переменных DLL объекту тестера для каждого из тестовых файлов (переменные, которые должны быть зарегистрированы, одинаковы для test_ .. . файл). Содержание списка используется для регистрации указанных данных.

Моя идея сделать это как-то так:

import pytest

class MyTester():
    def __init__(self, arg = ["var0", "var1"]):
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

# located in conftest.py (because other test will reuse it)

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester()
    return _tester

# located in test_...py

# @pytest.mark.usefixtures("tester") 
class TestIt():

    # def __init__(self):
    #     self.args_for_tester = ["var1", "var2"]
    #     # how to pass this list to the tester fixture?

    def test_tc1(self, tester):
       tester.dothis()
       assert 0 # for demo purpose

    def test_tc2(self, tester):
       tester.dothat()
       assert 0 # for demo purpose

Можно ли так этого добиться или есть еще более элегантный способ?

Обычно я мог сделать это для каждого метода тестирования с помощью какой-либо функции настройки (в стиле xUnit). Но я хочу получить какое-то повторное использование. Кто-нибудь знает, возможно ли это вообще со светильниками?

Я знаю, что могу сделать что-то вроде этого: (из документации)

@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"])

Но мне нужна параметризация прямо в тестовом модуле. Можно ли получить доступ к атрибуту params прибора из тестового модуля?

Мэгги
источник

Ответы:

101

Обновление: поскольку это принятый ответ на этот вопрос, и он по-прежнему иногда получает одобрение, я должен добавить обновление. Хотя мой первоначальный ответ (ниже) был единственным способом сделать это в более старых версиях pytest , как другие уже отметил pytest теперь поддерживает косвенную параметризацию светильников. Например, вы можете сделать что-то вроде этого (через @imiric):

# test_parameterized_fixture.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [True, False], indirect=['tester'])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture.py::TestIt::test_tc1[True] PASSED                                                                                                                    [ 50%]
test_parameterized_fixture.py::TestIt::test_tc1[False] FAILED

Однако, хотя эта форма косвенной параметризации является явной, как отмечает @Yukihiko Shinoda , теперь она поддерживает форму неявной косвенной параметризации (хотя я не смог найти очевидной ссылки на это в официальных документах):

# test_parameterized_fixture2.py
import pytest

class MyTester:
    def __init__(self, x):
        self.x = x

    def dothis(self):
        assert self.x

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [True, False])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
$ pytest -v test_parameterized_fixture2.py
================================================================================= test session starts =================================================================================
platform cygwin -- Python 3.6.8, pytest-5.3.1, py-1.8.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: .
collected 2 items

test_parameterized_fixture2.py::TestIt::test_tc1[True] PASSED                                                                                                                   [ 50%]
test_parameterized_fixture2.py::TestIt::test_tc1[False] FAILED

Я не знаю точно, какова семантика этой формы, но похоже, что она pytest.mark.parametrizeраспознает, что, хотя test_tc1метод не принимает аргумент с именем tester_arg, его использует testerприбор, который он использует, поэтому он передает параметризованный аргумент через testerприбор.


У меня была аналогичная проблема - у меня вызывается прибор test_package, и позже я хотел иметь возможность передавать необязательный аргумент этому приспособлению при его запуске в определенных тестах. Например:

@pytest.fixture()
def test_package(request, version='1.0'):
    ...
    request.addfinalizer(fin)
    ...
    return package

(Для этих целей не имеет значения, что делает прибор или какой тип возвращаемого объекта package).

Тогда было бы желательно каким-то образом использовать это приспособление в тестовой функции таким образом, чтобы я мог также указать versionаргумент этого приспособления для использования с этим тестом. В настоящее время это невозможно, хотя это может быть хорошей функцией.

Тем временем было достаточно легко заставить мой прибор просто возвращать функцию, которая выполняет всю работу, которую ранее выполняла прибор, но позволяет мне указать versionаргумент:

@pytest.fixture()
def test_package(request):
    def make_test_package(version='1.0'):
        ...
        request.addfinalizer(fin)
        ...
        return test_package

    return make_test_package

Теперь я могу использовать это в своей тестовой функции, например:

def test_install_package(test_package):
    package = test_package(version='1.1')
    ...
    assert ...

и так далее.

Попытка решения OP была направлена ​​в правильном направлении, и, как следует из ответа @ hpk42 , MyTester.__init__можно было просто сохранить ссылку на запрос, например:

class MyTester(object):
    def __init__(self, request, arg=["var0", "var1"]):
        self.request = request
        self.arg = arg
        # self.use_arg_to_init_logging_part()

    def dothis(self):
        print "this"

    def dothat(self):
        print "that"

Затем используйте это, чтобы реализовать приспособление, например:

@pytest.fixture()
def tester(request):
    """ create tester object """
    # how to use the list below for arg?
    _tester = MyTester(request)
    return _tester

При желании MyTesterкласс можно немного реструктурировать, чтобы его .argsатрибут можно было обновить после того, как он был создан, чтобы настроить поведение для отдельных тестов.

Iguananaut
источник
Спасибо за подсказку с функцией внутри приспособления. Прошло некоторое время, прежде чем я снова смог поработать над этим, но это очень полезно!
Мэгги
2
Хороший короткий пост по этой теме: alysivji.github.io/pytest-fixures-with-function-arguments.html
Мэгги,
Вы не получаете сообщение об ошибке: «Приспособления не предназначены для прямого вызова, они создаются автоматически, когда тестовые функции запрашивают их как параметры»?
nz_21
154

Это фактически поддерживается изначально в py.test через косвенную параметризацию .

В вашем случае у вас будет:

@pytest.fixture
def tester(request):
    """Create tester object"""
    return MyTester(request.param)


class TestIt:
    @pytest.mark.parametrize('tester', [['var1', 'var2']], indirect=True)
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
imiric
источник
Ах, это довольно мило (я думаю, что ваш пример может быть немного устаревшим - он отличается от примеров в официальных документах). Это относительно новая функция? Я никогда с этим не сталкивался. Это тоже хорошее решение проблемы - в некотором смысле лучше, чем мой ответ.
Iguananaut
2
Я попытался использовать это решение, но возникли проблемы с передачей нескольких параметров или использованием имен переменных, отличных от запроса. В итоге я использовал решение @Iguananaut.
Victor Uriarte
42
Это должен быть принятый ответ. Официальная документация по indirectключевому слову аргумента заведомо скудна и недружелюбна, что , вероятно , объясняет неизвестность этой важной техники. Я несколько раз просматривал сайт py.test в поисках этой самой функции - только чтобы оказаться пустым, старым и сбитым с толку. Горечь - это место, известное как непрерывная интеграция. Спасибо Odin за Stackoverflow.
Сесил Карри
1
Обратите внимание, что этот метод изменяет имя ваших тестов, чтобы включить параметр, который может или не может быть желательным. test_tc1становится test_tc1[tester0].
jjj
1
Итак, indirect=Trueпередаёт параметры всем вызываемым приборам, верно? Потому что в документации явно указаны приборы для косвенной параметризации, например, для прибора с именем x:indirect=['x']
winklerrr
11

Вы можете получить доступ к запрашивающему модулю / классу / функции из функций фикстуры (и, следовательно, из вашего класса тестера), см. Взаимодействие с запросом тестового контекста из функции фикстуры . Таким образом, вы можете объявить некоторые параметры в классе или модуле, и прибор тестера сможет их подобрать.

hpk42
источник
3
Я знаю, что могу сделать что-то вроде этого: (из документации) @ pytest.fixture (scope = "module", params = ["merlinux.eu", "mail.python.org"]) Но мне нужно сделать это в тестовый модуль. Как я могу динамически добавлять параметры в приборы?
Мэгги 07
2
Дело не в том, чтобы взаимодействовать с запросом тестового контекста от функции фикстуры, а в том, чтобы иметь четко определенный способ передачи аргументов в функцию фиксации. Функция Fixture не должна знать о типе запрашиваемого тестового контекста только для того, чтобы иметь возможность получать аргументы с согласованными именами. Например, кто-то хотел бы иметь возможность писать, @fixture def my_fixture(request)а затем @pass_args(arg1=..., arg2=...) def test(my_fixture)и получать эти аргументы my_fixture()вот так arg1 = request.arg1, arg2 = request.arg2. Возможно ли что-то подобное в py.test сейчас?
Петр Доброгост,
7

Я не смог найти ни одного документа, однако, похоже, он работает в последней версии pytest.

@pytest.fixture
def tester(tester_arg):
    """Create tester object"""
    return MyTester(tester_arg)


class TestIt:
    @pytest.mark.parametrize('tester_arg', [['var1', 'var2']])
    def test_tc1(self, tester):
       tester.dothis()
       assert 1
Юкихико Шинода
источник
Спасибо, что указали на это - это кажется самым чистым решением из всех. Я не думаю, что раньше это было возможно в более ранних версиях, но теперь ясно, что это возможно. Вы знаете, упоминается ли эта форма где-нибудь в официальных документах ? Я не нашел ничего похожего, но он явно работает. Я обновил свой ответ, включив этот пример, спасибо.
Игуананавт
1
Я думаю, что в этой функции это будет невозможно, если вы посмотрите на github.com/pytest-dev/pytest/issues/5712 и связанный (объединенный) PR.
Надеж,
Это было отменено github.com/pytest-dev/pytest/pull/6914
Maspe36
1
Чтобы уточнить, @ Maspe36 указывает, что PR, связанный пользователем, Nadègeбыл отменен. Таким образом, эта недокументированная функция (я думаю, что она все еще недокументирована?) Все еще существует.
blthayer
6

Чтобы немного улучшить ответ imiric : еще один элегантный способ решить эту проблему - создать « фиксации параметров». Я лично предпочитаю это indirectфункции pytest. Эта функция доступна от Sup3rGeopytest_cases , а первоначальная идея была предложена .

import pytest
from pytest_cases import param_fixture

# create a single parameter fixture
var = param_fixture("var", [['var1', 'var2']], ids=str)

@pytest.fixture
def tester(var):
    """Create tester object"""
    return MyTester(var)

class TestIt:
    def test_tc1(self, tester):
       tester.dothis()
       assert 1

Обратите внимание, что это pytest-casesтакже обеспечивает, @pytest_fixture_plusчто позволяет вам использовать метки параметризации на ваших осветительных приборах, и @cases_dataчто позволяет вам получать параметры из функций в отдельном модуле. Подробности см. В документе . Кстати, я автор;)

smarie
источник
1
Кажется, теперь это работает и в простом pytest (у меня v5.3.1). То есть я смог заставить это работать без param_fixture. Смотрите этот ответ . Однако я не нашел в документации подобного примера; ты что-нибудь знаешь об этом?
Игуананавт
спасибо за информацию и ссылку! Я понятия не имел, что это возможно. Давайте подождем официальной документации, чтобы понять, что они имеют в виду.
Smarie
2

Я сделал забавный декоратор, который позволяет писать такие приспособления:

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"

Здесь слева от /вас есть другие приборы, а справа у вас есть параметры, которые предоставляются с использованием:

@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"

Это работает так же, как аргументы функции. Если вы не укажете ageаргумент, 69вместо него будет использоваться значение по умолчанию ,,. если вы не предоставите nameили опустите dog.argumentsдекоратор, вы получите обычный TypeError: dog() missing 1 required positional argument: 'name'. Если у вас есть другой прибор, который принимает аргументы name, он не конфликтует с этим.

Также поддерживаются асинхронные фикстуры.

Кроме того, это дает вам хороший план настройки:

$ pytest test_dogs_and_owners.py --setup-plan

SETUP    F dog['Buddy', age=7]
...
SETUP    F dog['Champion']
SETUP    F owner (fixtures used: dog)['John Travolta']

Полный пример:

from plugin import fixture_taking_arguments

@fixture_taking_arguments
def dog(request, /, name, age=69):
    return f"{name} the dog aged {age}"


@fixture_taking_arguments
def owner(request, dog, /, name="John Doe"):
    yield f"{name}, owner of {dog}"


@dog.arguments("Buddy", age=7)
def test_with_dog(dog):
    assert dog == "Buddy the dog aged 7"


@dog.arguments("Champion")
class TestChampion:
    def test_with_dog(self, dog):
        assert dog == "Champion the dog aged 69"

    def test_with_default_owner(self, owner, dog):
        assert owner == "John Doe, owner of Champion the dog aged 69"
        assert dog == "Champion the dog aged 69"

    @owner.arguments("John Travolta")
    def test_with_named_owner(self, owner):
        assert owner == "John Travolta, owner of Champion the dog aged 69"

Код декоратора:

import pytest
from dataclasses import dataclass
from functools import wraps
from inspect import signature, Parameter, isgeneratorfunction, iscoroutinefunction, isasyncgenfunction
from itertools import zip_longest, chain


_NOTHING = object()


def _omittable_parentheses_decorator(decorator):
    @wraps(decorator)
    def wrapper(*args, **kwargs):
        if not kwargs and len(args) == 1 and callable(args[0]):
            return decorator()(args[0])
        else:
            return decorator(*args, **kwargs)
    return wrapper


@dataclass
class _ArgsKwargs:
    args: ...
    kwargs: ...

    def __repr__(self):
        return ", ".join(chain(
               (repr(v) for v in self.args), 
               (f"{k}={v!r}" for k, v in self.kwargs.items())))


def _flatten_arguments(sig, args, kwargs):
    assert len(sig.parameters) == len(args) + len(kwargs)
    for name, arg in zip_longest(sig.parameters, args, fillvalue=_NOTHING):
        yield arg if arg is not _NOTHING else kwargs[name]


def _get_actual_args_kwargs(sig, args, kwargs):
    request = kwargs["request"]
    try:
        request_args, request_kwargs = request.param.args, request.param.kwargs
    except AttributeError:
        request_args, request_kwargs = (), {}
    return tuple(_flatten_arguments(sig, args, kwargs)) + request_args, request_kwargs


@_omittable_parentheses_decorator
def fixture_taking_arguments(*pytest_fixture_args, **pytest_fixture_kwargs):
    def decorator(func):
        original_signature = signature(func)

        def new_parameters():
            for param in original_signature.parameters.values():
                if param.kind == Parameter.POSITIONAL_ONLY:
                    yield param.replace(kind=Parameter.POSITIONAL_OR_KEYWORD)

        new_signature = original_signature.replace(parameters=list(new_parameters()))

        if "request" not in new_signature.parameters:
            raise AttributeError("Target function must have positional-only argument `request`")

        is_async_generator = isasyncgenfunction(func)
        is_async = is_async_generator or iscoroutinefunction(func)
        is_generator = isgeneratorfunction(func)

        if is_async:
            @wraps(func)
            async def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_async_generator:
                    async for result in func(*args, **kwargs):
                        yield result
                else:
                    yield await func(*args, **kwargs)
        else:
            @wraps(func)
            def wrapper(*args, **kwargs):
                args, kwargs = _get_actual_args_kwargs(new_signature, args, kwargs)
                if is_generator:
                    yield from func(*args, **kwargs)
                else:
                    yield func(*args, **kwargs)

        wrapper.__signature__ = new_signature
        fixture = pytest.fixture(*pytest_fixture_args, **pytest_fixture_kwargs)(wrapper)
        fixture_name = pytest_fixture_kwargs.get("name", fixture.__name__)

        def parametrizer(*args, **kwargs):
            return pytest.mark.parametrize(fixture_name, [_ArgsKwargs(args, kwargs)], indirect=True)

        fixture.arguments = parametrizer

        return fixture
    return decorator
белка
источник