188

我正在使用 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"])

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

4

9 回答 9

222

这实际上是通过间接参数化在 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
于 2015-11-23T19:34:42.043 回答
167

更新:由于这是对这个问题的公认答案,并且有时仍会被赞成,我应该添加一个更新。尽管我的原始答案(如下)是在旧版本的 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在创建后更新其属性,以调整单个测试的行为。

于 2015-02-17T20:36:40.410 回答
28

我找不到任何文档,但是,它似乎适用于最新版本的 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
于 2020-02-10T10:59:50.113 回答
12

您可以从夹具函数(因此从您的测试器类)访问请求模块/类/函数,请参阅与从夹具函数请求测试上下文交互。因此,您可以在类或模块上声明一些参数,并且测试器夹具可以拾取它。

于 2013-08-07T08:44:45.963 回答
11

改进一点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还提供@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这些函数可能被分组在一个类甚至一个单独的模块中。有关详细信息,请参阅文档。顺便说一句,我是作者;)

于 2019-03-28T09:30:34.713 回答
8

我做了一个有趣的装饰器,它允许编写这样的固定装置:

@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
于 2020-06-10T23:14:42.343 回答
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

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

于 2021-07-07T13:01:03.403 回答
0

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

@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()

这样testertest_tc1 和 test_tc2 的对象都将使用tester_args参数进行初始化。

您还可以使用:

  • request.function访问 test_tc1 函数,
  • request.instance访问 TestIt 类实例,
  • request.module访问模块 TestIt 定义在
  • 等(请参阅request文档)
于 2020-10-09T13:48:14.103 回答
0

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

例如:

@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 客户端的夹具并想要测试不同的可能输出时,我将它用于我的测试,我可以使用标记决定每个测试的输出。

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

于 2021-10-25T10:21:55.543 回答