py.test:将参数传递给fixture函数

时间:2013-08-02 08:11:46

标签: python fixtures pytest

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

由于测试对象是获取DLL变量和函数的引用,我需要将DLL的变量列表传递给每个测试文件的测试对象(要记录的变量对于测试_...文件)。 列表的内容应用于记录指定的数据。

我的想法是以某种方式这样做:

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-style)为每个测试方法执行此操作。但我希望获得某种重用。有没有人知道这是否可以使用灯具?

我知道我可以这样做:(来自文档)

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

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

7 个答案:

答案 0 :(得分:76)

我遇到了类似的问题 - 我有一个名为test_package的夹具,后来我想在特定的测试中运行它时能够将一个可选参数传递给该夹具。例如:

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

(对于这些目的而言,夹具的功能或返回的package)的对象类型无关紧要。

然后需要以某种方式在测试函数中使用此fixture,这样我还可以指定该fixture的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属性后对其进行更新,以调整各个测试的行为。

答案 1 :(得分:14)

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

答案 2 :(得分:11)

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

答案 3 :(得分:4)

我做了一个有趣的装饰器,可以编写这样的装置:

@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

答案 4 :(得分:1)

要提高一点imiric's answer:解决此问题的另一种优雅方法是创建“参数固定装置”。我个人更喜欢indirect的{​​{1}}功能。 pytest_cases可以使用此功能,Sup3rGeo提出了最初的想法。

pytest

请注意,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。我是作者;)

答案 5 :(得分:1)

您还可以使用闭包,这将使您对参数进行更全面的命名和控制。它们比隐式参数化中使用的 import matplotlib.pyplot as plt import numpy as np import scipy.signal as sp n=8 data = np.random.rand(64,64) fig, ax1 = plt.subplots(num=444, nrows=1, ncols=1, figsize=(8, 6)) p1 = ax1.pcolor(np.flipud(data)) fig.colorbar(p1, ax=ax1, orientation='vertical', format='%.1f') ax1.set_xlabel("X") ax1.set_title(data.shape); data2 = sp.decimate(data, n, n=None, ftype='iir', axis=0, zero_phase=True) fig, ax1 = plt.subplots(num=445, nrows=1, ncols=1, figsize=(8, 6)) p1 = ax1.pcolor(np.flipud(data2)) fig.colorbar(p1, ax=ax1, orientation='vertical', format='%.1f') ax1.set_xlabel("X") ax1.set_title(data2.shape); data3 = sp.decimate(data2, n, n=None, ftype='iir', axis=1, zero_phase=True) fig, ax1 = plt.subplots(num=446, nrows=1, ncols=1, figsize=(8, 6)) p1 = ax1.pcolor(np.flipud(data3)) fig.colorbar(p1, ax=ax1, orientation='vertical', format='%.0f') ax1.set_xlabel("X") ax1.set_title(data3.shape); 参数更“显式”:

request

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

答案 6 :(得分:0)

另一种方法是使用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()

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

您还可以使用:

  • request.function访问test_tc1函数,
  • request.instance访问TestIt类实例,
  • request.module访问模块TestIt是在
  • 中定义的
  • 等(请参阅request文档)