如何在pytest中将几个参数化的夹具连接成一个新的夹具?

Posted

技术标签:

【中文标题】如何在pytest中将几个参数化的夹具连接成一个新的夹具?【英文标题】:How to concatenate several parametrized fixtures into a new fixture in py.test? 【发布时间】:2014-08-12 00:04:41 【问题描述】:

如果我有两个参数化的夹具,我如何创建一个单独的测试函数,首先用一个夹具的实例调用,然后用另一个夹具的实例调用?

我想创建一个以某种方式连接两个现有夹具的新夹具是有意义的。这适用于“普通”灯具,但我似乎无法让它与参数化灯具一起使用。

这是我尝试过的一个简化示例:

import pytest

@pytest.fixture(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest.fixture(params=[1, 2])
def upper(request):
    return "I" * request.param

@pytest.fixture(params=['lower', 'upper'])
def all(request):
    return request.getfuncargvalue(request.param)

def test_all(all):
    assert 0, all

当我运行它时,我得到了这个错误:

request = <SubRequest 'lower' for <Function 'test_all[lower]'>>

    @pytest.fixture(params=[1, 2, 3])
    def lower(request):
>       return "i" * request.param
E       AttributeError: 'SubRequest' object has no attribute 'param'

...upper() 出现同样的错误。

我做错了什么?

我该如何解决这个问题?


更新:

有一个 PyTest 插件可以用来解决这个问题:https://github.com/TvoroG/pytest-lazy-fixture。

pip-安装此插件后,对上述代码唯一需要的改动如下:

@pytest.fixture(params=[pytest.lazy_fixture('lower'),
                        pytest.lazy_fixture('upper')])
def all(request):
    return request.param

但请注意,在某些复杂的情况下它不起作用:

https://github.com/pytest-dev/pytest/issues/3244#issuecomment-369836702

相关的 PyTest 问题:

https://github.com/pytest-dev/pytest/issues/349 https://github.com/pytest-dev/pytest/issues/460 https://github.com/pytest-dev/pytest/issues/3244

【问题讨论】:

我看到 py.test 跟踪器上有一个 issue 可能会解决我的问题,但 py.test 开发人员还没有回复。 有another issue 似乎与我的问题有关,但也没有回应...... pytest-lazy-fixture 插件可以让你做到这一点。 @ChristianLong 感谢您的提示!我在上面添加了一些信息。 【参考方案1】:

现在pytest-cases 中有一个可用的解决方案,名为fixture_union。以下是创建示例中请求的夹具联合的方法:

from pytest_cases import fixture_union, pytest_fixture_plus

@pytest_fixture_plus(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest_fixture_plus(params=[1, 2])
def upper(request):
    return "I" * request.param

fixture_union('all', ['lower', 'upper'])

def test_all(all):
    print(all)

它按预期工作:

<...>::test_all[lower-1] 
<...>::test_all[lower-2] 
<...>::test_all[lower-3] 
<...>::test_all[upper-1] 
<...>::test_all[upper-2] 

请注意,我在上面的示例中使用了pytest_fixture_plus,因为如果您使用pytest.fixture,您将不得不自己处理未实际使用夹具的情况。这是按如下方式完成的,例如对于upper 夹具:

import pytest
from pytest_cases import NOT_USED

@pytest.fixture(params=[1, 2])
def upper(request):
    # this fixture does not use pytest_fixture_plus 
    # so we have to explicitly discard the 'NOT_USED' cases
    if request.param is not NOT_USED:
        return "I" * request.param

详情请见documentation。 (顺便说一句,我是作者;))

【讨论】:

【参考方案2】:

我有完全相同的question(并收到了类似但不同的answer)。我能想出的最好的解决方案是重新考虑我如何参数化我的测试。我没有使用多个具有兼容输出的固定装置,而是将固定装置用作常规函数,并将您的元固定装置参数化以接受函数名称和参数:

import pytest

def lower(n):
    return 'i' * n

def upper(n):
    return 'I' * n

@pytest.fixture(params=[
    (lower, 1),
    (lower, 2),
    (upper, 1),
    (upper, 2),
    (upper, 3),
])
def all(request):
    func, *n = request.param
    return func(*n)

def test_all(all):
    ...

在您的特定情况下,将n 解压缩到列表中并使用* 传递它有点过分,但它提供了通用性。我的案例有所有接受不同参数列表的固定装置。

在 pytest 允许我们正确链接固定装置之前,这是我想出的唯一方法来运行 5 个测试而不是在您的情况下运行 12 个。您可以使用类似的方法缩短列表

@pytest.fixture(params=[
    *[(lower, i) for i in range(1, 3)],
    *[(upper, i) for i in range(1, 4)],
])

这样做有一个实际的优势。如果您的管道中有其他依赖项,您可以选择要执行特殊操作的测试,例如 XFAIL,而不会影响整个其他测试。

【讨论】:

嗯,灯具的要点是它们可以重复使用,对吧?如果我创建了一个参数化的夹具,我还想重新使用参数化(因为它是夹具的一部分)。你建议的方式,我不能重复使用参数化。想象一下有更多的装置,如lowerupper,我想对它们进行几种不同的组合。 lazy_fixture 插件让我可以完全做到这一点,而无需手动重复任何内容。【参考方案3】:

它并不美丽,但也许今天你知道更好的方法。

'all' 夹具内的请求对象只知道自己的参数:'lower','upper'。一种方式using fixtures from a fixture function。

import pytest

@pytest.fixture(params=[1, 2, 3])
def lower(request):
    return "i" * request.param

@pytest.fixture(params=[1, 2])
def upper(request):
    return "I" * request.param

@pytest.fixture(params=['lower', 'upper'])
def all(request, lower, upper):
    if request.param == 'lower':
        return lower
    else:
        return upper

def test_all(all):
    assert 0, all

【讨论】:

感谢您的回答!但是同时使用lowerupper 作为all() 的函数参数运行lower()upper() 的所有组合(我猜更准确地说是笛卡尔积)的测试,即6 次。将两个 params 添加到 all() 会使数字翻倍,即 test_all() 运行 12 次。我只想连接lower()upper(),即只运行5次测试。

以上是关于如何在pytest中将几个参数化的夹具连接成一个新的夹具?的主要内容,如果未能解决你的问题,请参考以下文章

如何参数化 Pytest 夹具

如何通过自定义装饰器提供非夹具 pytest 参数?

如何绕过 pytest 夹具装饰器?

40-pytest-Hook函数之参数化生成用例

如何从范围=会话的pytest夹具返回多个值

如何使用外部夹具跳过 pytest?