【问题标题】:Pass a parameter to a fixture function将参数传递给夹具函数
【发布时间】:2013-08-03 10:18:28
【问题描述】:

我正在使用 py.test 来测试一些包装在 python 类 MyTester 中的 DLL 代码。 出于验证目的,我需要在测试期间记录一些测试数据,然后再进行更多处理。由于我有很多 test_... 文件,因此我想在大多数测试中重用测试器对象创建(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"])

但我需要直接在测试模块中进行参数化。 是否可以从测试模块访问fixture的params属性?

【问题讨论】:

    标签: python fixtures pytest


    【解决方案1】:

    这实际上在 py.test 中通过indirect parametrization 原生支持。

    在你的情况下,你会:

    @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 的解决方案。
    • 这应该是公认的答案。 诚然,indirect 关键字参数的official documentation 是稀疏且不友好的,这可能是这项基本技术晦涩难懂的原因。我已经多次搜索 py.test 网站来寻找这个功能——结果却是空的、老的和困惑的。苦涩是一个地方,被称为持续集成。 感谢 Odin 使用 Stackoverflow。
    • 请注意,此方法会更改测试的名称以包含参数,这可能需要也可能不需要。 test_tc1 变为 test_tc1[tester0]
    • 所以indirect=True 将参数交给所有被调用的灯具,对吧?因为documentation 明确命名了用于间接参数化的夹具,例如对于名为 x 的夹具:indirect=['x']
    • 好的,所以 TrueFalse 也适用于 indirect 关键字根据 official documentation 关于参数化。
    【解决方案2】:

    更新:由于这是对这个问题的公认答案,并且有时仍会获得支持,因此我应该添加更新。尽管我的原始答案(如下)是在旧版本的 pytest 中执行此操作的唯一方法,因为 othersnoted 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 points out 它现在支持一种隐式间接参数化的形式(尽管我在官方文档中找不到任何明显的参考):

    # 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
    

    (对于这些目的而言,fixture 的作用或返回的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 的 answer 所暗示的,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 属性,以调整各个测试的行为。

    【讨论】:

    • 感谢灯具内部功能的提示。确实需要一些时间才能再次进行此操作,但这非常有用!
    • 一篇关于这个主题的简短文章:alysivji.github.io/pytest-fixures-with-function-arguments.html
    • 你没有收到错误提示:“Fixtures 不是直接调用的,而是在测试函数请求它们作为参数时自动创建的。”?
    【解决方案3】:

    我找不到任何文档,但是,它似乎可以在最新版本的 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
    

    【讨论】:

    • 感谢您指出这一点——这似乎是最干净的解决方案。我认为这在以前的版本中是不可能的,但现在很明显。您知道official docs 中是否有提到此表格吗?我找不到任何类似的东西,但它显然有效。我已更新 my answer 以包含此示例,谢谢。
    • 如果你看一下github.com/pytest-dev/pytest/issues/5712 和相关的(合并的)PR,我认为这在功能中是不可能的。
    • 澄清一下,@Maspe36 表示Nadège 链接的 PR 已恢复。因此,这个未记录的功能(我认为它仍然是未记录的?)仍然存在。
    【解决方案4】:

    您可以从夹具函数(因此从您的 Tester 类)访问请求模块/类/函数,请参阅interacting with requesting test context from a fixture function。因此,您可以在类或模块上声明一些参数,测试器夹具可以获取它。

    【讨论】:

    • 我知道我可以这样做:(来自文档)@pytest.fixture(scope="module", params=["merlinux.eu", "mail.python.org"] ) 但我需要在测试模块中进行。如何动态添加参数到灯具?
    • 重点不是必须与从夹具函数请求测试上下文交互,而是要有一个定义明确的方法来将参数传递给夹具函数。 Fixture 函数不必知道请求测试上下文的类型只是为了能够接收具有商定名称的参数。例如,希望能够编写@fixture def my_fixture(request) 然后@pass_args(arg1=..., arg2=...) def test(my_fixture) 并在my_fixture() 中获取这些参数,例如arg1 = request.arg1, arg2 = request.arg2。现在在 py.test 中可以做到这样的事情吗?
    【解决方案5】:

    稍微改进一下imiric's answer:解决这个问题的另一种优雅方法是创建“参数夹具”。我个人更喜欢它而不是pytestindirect 功能。这个功能可以从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 还提供了@fixture,允许您直接在您的灯具上使用参数化标记,而不必使用@pytest.fixture(params=...)

    from pytest_cases import fixture, parametrize
    
    @fixture
    @parametrize("var", [['var1', 'var2']], ids=str)
    def tester(var):
        """Create tester object"""
        return MyTester(var)
    

    @parametrize_with_cases,它允许您从“案例函数”中获取参数,这些函数可能被分组在一个类甚至一个单独的模块中。有关详细信息,请参阅doc。顺便说一句,我是作者;)

    【讨论】:

    • 这似乎现在也可以在普通的 pytest 中使用(我有 v5.3.1)。也就是说,我能够在没有param_fixture 的情况下完成这项工作。见this answer。我在文档中找不到任何类似的例子;你知道吗?
    • 感谢您的信息和链接!我不知道这是可行的。让我们等待官方文档,看看他们的想法。
    【解决方案6】:

    我制作了一个有趣的装饰器,可以像这样编写固定装置:

    @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
    

    【讨论】:

    • 我看起来真的很像 pytest!您是否打算将其提交给上游(进入 pytest)?
    • @GeorgeShuklin 好吧,我继续并为此打开了一个问题,以及更多疯狂的想法github.com/pytest-dev/pytest/issues/8109
    【解决方案7】:

    您还可以使用闭包,这将使您对参数进行更全面的命名和控制。它们比 隐式参数化中使用的request 参数更“显式”:

    
    @pytest.fixture
    def tester():
        # Create a closure on the Tester object
        def _tester(first_param, second_param):
            # use the above params to mock and instantiate things
            return MyTester(first_param, second_param)
        
        # Pass this closure to the test
        yield _tester 
    
    
    @pytest.mark.parametrize(['param_one', 'param_two'], [(1,2), (1000,2000)])
    def test_tc1(tester, param_one, param_two):
        # run the closure now with the desired params
        my_tester = tester(param_one, param_two)
        # assert code here
    

    我用它来构建可配置的装置

    【讨论】:

      【解决方案8】:

      另一种方法是使用request 对象来访问定义在定义测试函数的模块或类中的变量。

      这样,如果您想为类/模块的所有测试函数传递相同的变量,您就不必在测试类的每个函数上重用 @pytest.mark.parametrize() 装饰器。

      类变量示例:

      @pytest.fixture
      def tester(request):
          """Create tester object"""
          return MyTester(request.cls.tester_args)
      
      
      class TestIt:
          tester_args = ['var1', 'var2']
      
          def test_tc1(self, tester):
             tester.dothis()
      
          def test_tc2(self, tester):
             tester.dothat()
      

      这样,test_tc1 和 test_tc2 的 tester 对象将使用 tester_args 参数进行初始化。

      你也可以使用:

      • request.function 访问 test_tc1 函数,
      • request.instance 访问 TestIt 类实例,
      • request.module访问模块TestIt定义在
      • 等。 (参考request 文档)

      【讨论】:

        【解决方案9】:

        另一种方法是使用自定义标记。它看起来比代码中的参数化更好,没有体现在测试名称中,也是可选的(如果不存在这样的标记,可以通过引发失败来定义为非可选)

        例如:

        @pytest.fixture
        def loaded_dll(request):
            dll_file = None
            for mark in request.node.iter_markers("dll_file"):
                if mark.args:
                    if dll_file is not None:
                        pytest.fail("Only one dll_file can be mentioned in marks")
                    dll_file = mark.args[0]
            if dll_file is None:
                pytest.fail("dll_file is a required mark")
            return some_dll_load(dll_file)
        
        @pytest.mark.dll_file("this.dll")
        def test_this_dll(loaded_dll):
            ...
        

        当我需要一个模拟 ssh 客户端的夹具并想测试不同的可能输出时,我将它用于我的测试,我可以使用标记决定每个测试的输出。

        请注意,如果是供个人使用,则不需要通过测试的故障保存机制。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2020-12-05
          • 2013-01-27
          • 2021-04-18
          • 1970-01-01
          相关资源
          最近更新 更多