使用返回列表的fixture来参数化pytest函数的最佳方法是什么?

时间:2018-03-05 19:11:55

标签: python pytest

现在我有一个看起来像这个人为例子的测试文件:

import pytest

def colors():
    # Expensive operation
    return ['red', 'yellow', 'blue']

@pytest.mark.parametrize('color', colors())
def test_colors(color):
    assert color != 'mauve'

这很好用,但由于colors()是一项昂贵的操作,我想利用pytest的缓存并使其成为一个会话范围的夹具。此外,我还想用其作为像

这样的夹具编写其他测试
def test_colors_list(colors):
    assert len(colors) == 3

理想情况下,我的测试文件看起来像

@pytest.fixture(scope='session')
def colors():
    # Expensive operation
    return ['red', 'yellow', 'blue']

@pytest.mark.parametrize('color', colors)
def test_colors(color):
    assert color != 'mauve'

def test_colors_list(colors):
    assert len(colors) == 3

但是这会导致错误,所以我没有正确地解决这个问题。

理想情况下,我还想在colors()中引用其他灯具,并且仍然参数化test_colors()以生成多个函数。

我应该编写这些测试的最佳方式是什么?

4 个答案:

答案 0 :(得分:2)

一种解决方案是在单独的函数中生成颜色,并将其结果用作灯具的params参数,然后可以多次使用

import pytest

def colors():
    print('expensive')
    return ['blue', 'red', 'mauve']

@pytest.fixture(params=colors())
def color(request):
    return request.param

def test_bla(color):
    print(color, end='')

def test_foo(color):
    print(color, end='')

如果您运行pytest -s,您只会在输出中看到字符串expensive

$py.test -sv
======================== test session starts =========================
platform linux -- Python 3.4.3, pytest-3.4.1, py-1.5.2, pluggy-0.6.0 -- /home/nils/test/bin/python3
cachedir: .pytest_cache
rootdir: /home/nils/test/bla, inifile:
collecting 0 items                                                   expensive
collected 6 items

test_bla.py::test_bla[blue] bluePASSED
test_bla.py::test_bla[red] redPASSED
test_bla.py::test_bla[mauve] mauvePASSED
test_bla.py::test_foo[blue] bluePASSED
test_bla.py::test_foo[red] redPASSED
test_bla.py::test_foo[mauve] mauvePASSED

====================== 6 passed in 0.04 seconds ======================

但是,昂贵的功能是在导入时运行的,这不是一个好的行为。

答案 1 :(得分:2)

围绕colors()结果的简单缓存工作适用于会话范围:

@pytest.fixture(scope='session')
def colors():
    try:
        return colors._res
    except AttributeError:
        # Expensive operation
        print()
        print('expensive')
        colors._res = ['red', 'yellow', 'blue']
        return colors._res

@pytest.mark.parametrize('color', colors())
def test_colors(color):
    assert color != 'mauve'

def test_colors_list(colors):
    assert len(colors) == 3

虽然函数colors()被调用了两次,但是对于所有测试,只进行一次昂贵的计算:

$ pytest -sv test_colors.py 
================================================= test session starts ==================================================
platform darwin -- Python 3.6.4, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 -- /Users/mike/miniconda3/envs/py36/bin/python
cachedir: .cache
rootdir: /Users/mike/tmp, inifile:
plugins: click-0.1, cov-2.4.0, mock-1.6.0, pylint-0.7.1, xdist-1.15.0, xonsh-0.5.8
collecting 0 items
expensive
collected 4 items 

test_colors.py::test_colors[red] PASSED
test_colors.py::test_colors[yellow] PASSED
test_colors.py::test_colors[blue] PASSED
test_colors.py::test_colors_list PASSED

答案 2 :(得分:1)

另一个解决方案是在会话夹具中生成颜色并在常规夹具中迭代返回的结果,然后您可以多次使用

import pytest

@pytest.fixture(scope='session')
def a():
    return 'purple'

@pytest.fixture(scope='session')
def colors(a):
    print('expensive')
    return ['blue', 'red', 'mauve', a]

@pytest.fixture(params=range(4))   # unfortunately we need to know the number of values returned by `colors()`
def color(request, colors):
    return colors[request.param]

def test_bla(color):
    print(color, end='')

def test_foo(color):
    print(color, end='')

如果您运行pytest -s,您只会在输出中看到字符串expensive

$ py.test  -sv
======================== test session starts =========================
platform linux -- Python 3.4.3, pytest-3.4.1, py-1.5.2, pluggy-0.6.0 -- cwd
cachedir: .pytest_cache
rootdir: cwd, inifile:
collected 8 items

test_bla.py::test_bla[0] expensive
bluePASSED
test_bla.py::test_bla[1] redPASSED
test_bla.py::test_bla[2] mauvePASSED
test_bla.py::test_bla[3] purplePASSED
test_bla.py::test_foo[0] bluePASSED
test_bla.py::test_foo[1] redPASSED
test_bla.py::test_foo[2] mauvePASSED
test_bla.py::test_foo[3] purplePASSED

====================== 8 passed in 0.03 seconds ======================

Plus:在导入时没有调用昂贵的函数(请参阅输出中出现expensive的位置),您可以在colors中使用其他会话装置

答案 3 :(得分:0)

语法错误可以解决如下:

<强> test_1.py:

import pytest

@pytest.fixture(scope='session')
def colors():
    # Expensive operation
    return ['red', 'yellow', 'blue']

@pytest.mark.parametrize('color',colors())
def test_colors(color):
    assert color != 'mauve'

def test_colors_list(colors):
    assert len(colors) == 3

但是,使用显示夹具设置和拆卸详细信息的--setup-show选项运行pytest中的上一个代码将显示以下内容:

test_1.py::test_colors[red]
      SETUP    F color[red]
        test_1.py::test_colors[red] (fixtures used: color)PASSED
      TEARDOWN F color[red]
test_1.py::test_colors[yellow]
      SETUP    F color[yellow]
        test_1.py::test_colors[yellow] (fixtures used: color)PASSED
      TEARDOWN F color[yellow]
test_1.py::test_colors[blue]
      SETUP    F color[blue]
        test_1.py::test_colors[blue] (fixtures used: color)PASSED
      TEARDOWN F color[blue]
test_1.py::test_colors_list
SETUP    S colors
        test_1.py::test_colors_list (fixtures used: colors)PASSED
TEARDOWN S colors

正如您在输出中所看到的,每个测试用例都会设置/拆卸颜色夹具,并为最终测试用例再次设置/拆卸颜色夹具。

为避免这种情况,您可以按照以下方式最低限度地重构代码:

<强> test_1.py:

@pytest.fixture(scope='session')
def colors():
    # Expensive operation
    return ['red', 'yellow', 'blue']

def test_colors(colors):
    for color in colors:
        assert color != 'mauve'

def test_colors_list(colors):
    assert len(colors) == 3

使用--setup-show再次运行pytest将输出以下内容:

test_1.py::test_colors
SETUP    S colors
        test_1.py::test_colors (fixtures used: colors)PASSED
test_1.py::test_colors_list
        test_1.py::test_colors_list (fixtures used: colors)PASSED
TEARDOWN S colors

您可以检查夹具是否仅为所有测试设置/拆卸一次。

您可以查看pytest docs

中的相关部分