将参数传递给灯具功能


114

我正在使用py.test来测试包装在python类MyTester中的某些DLL代码。为了进行验证,我需要在测试期间记录一些测试数据,然后再进行更多处理。由于我有许多test _...文件,因此我想对大多数测试重用测试器对象的创建(例如MyTester的实例)。

由于tester对象是获得DLL变量和函数的引用的对象,因此我需要将DLL变量列表传递给每个测试文件的tester对象(要记录的变量对于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属性?

Answers:


100

更新:由于这是该问题的公认答案,并且有时仍然会被反对,因此我应该添加一个更新。尽管我的原始答案(如下)是在较旧版本的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_argtester夹具却可以,因此它通过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在创建其属性后可以对其进行更新,以调整各个测试的行为。


感谢您对灯具内部功能的提示。花了一些时间才可以再次进行此操作,但这非常有用!
玛姬2015年


是否不会出现错误信息:“夹具不是要直接调用的,而是在测试功能将其要求为参数时自动创建的。”
nz_21,19年

153

实际上,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

嗯,这很好(我认为您的示例可能有些过时了-与官方文档中的示例有所不同)。这是一个相对较新的功能吗?我从来没有遇到过。这也是解决问题的好方法-在某些方面比我的答案更好。
Iguananaut

2
我尝试使用此解决方案,但在传递多个参数或使用请求以外的变量名时遇到了问题。我最终使用了@Iguananaut的解决方案。
维克托·乌里亚特

42
这应该是公认的答案。关键字参数的官方文档indirect认为是稀疏且不友好的,这可能解释了此基本技术的晦涩之处。对于此功能,我已经多次搜索py.test网站-只是空着,变老了而且迷惑了。苦涩是被称为持续整合的地方。感谢Odin提供Stackoverflow。
塞西尔·库里

1
请注意,此方法会将测试名称更改为包含参数,这可能需要也可能不需要。test_tc1成为test_tc1[tester0]
jjj

1
那么indirect=True将参数交给所有调用的灯具,对吗?因为文档中明确指定了用于间接参数化的灯具,例如名为xindirect=['x']
winklerrr,

11

您可以从Fixture函数(从而从Tester类)访问请求的模块/类/函数,请参见与来自Fixture函数的请求测试上下文进行交互。因此,您可以在类或模块上声明一些参数,然后测试仪固定装置即可进行拾取。


3
我知道我可以做这样的事情:(来自文档)@ pytest.fixture(scope =“ module”,params = [“ merlinux.eu”,“ mail.python.org”])但是我需要在测试模块。如何动态向灯具添加参数?
麦琪

2
关键是不必从Fixture函数中请求测试上下文进行交互,而是要有一种明确定义的方法来将参数传递给Fixture函数。夹具功能不必仅知道能够接收具有商定名称的参数的请求测试上下文的类型。例如,一个人希望能够编写@fixture def my_fixture(request)然后然后像这样@pass_args(arg1=..., arg2=...) def test(my_fixture)获得这些args 。现在在py.test中有可能出现这种情况吗?my_fixture()arg1 = request.arg1, arg2 = request.arg2
Piotr Dobrogost

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

感谢您指出这一点-这似乎是所有方法中最干净的解决方案。我认为以前在以前的版本中不可能做到这一点,但是很明显现在已经可以了。您知道官方文档中是否提及此表格吗?我找不到任何类似的东西,但是显然可以。我已经更新了答案,以包含此示例,谢谢。
Iguananaut

1
我认为如果您查看github.com/pytest-dev/pytest/issues/5712和相关的(合并的)PR ,则该功能将无法实现。
Nadège11


1
为澄清起见,@ Maspe36表示链接的PR Nadège已还原。因此,此未记录的功能(我认为它仍未记录?)仍然有效。
blthayer

6

改善imiric的答案:解决此问题的另一种优雅方法是创建“参数夹具”。我个人更喜欢它的indirect功能pytest。此功能可从中获得pytest_cases,最初的想法由Sup3rGeo提出。

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允许您从单独模块中的函数中获取参数。有关详细信息,请参见doc。我是作者;)


1
现在似乎也可以在纯pytest中运行(我有v5.3.1)。也就是说,没有,我就可以使它工作param_fixture。看到这个答案。我在文档中找不到类似的例子;你对此一无所知吗?
Iguananaut

感谢您的信息和链接!我不知道这是可行的。让我们等待正式的文档,看看他们的想法。
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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.