pytest的懒参数化

时间:2019-01-12 20:20:56

标签: python python-3.x pytest

在pytest中对测试和固定装置进行参数化时,pytest似乎急切地评估所有参数并在开始执行测试之前构建一些测试列表数据结构。

这是2种情况下的问题:

  1. 当您有很多参数值(例如来自生成器的参数)时-生成器和测试本身可能会快速运行,但所有这些参数值都会耗尽所有内存
  2. 在对具有不同类型的昂贵资源的灯具进行参数化时,您只能负担同时运行一种资源(例如,因为它们在同一端口或类似的端口上监听)

因此,我的问题是:可能要告诉pytest动态(即懒惰地)评估参数吗?

3 个答案:

答案 0 :(得分:1)

关于您的第二个问题-在manual的评论链接中提出的建议似乎恰好应该解决。它允许“仅在运行实际测试时设置昂贵的资源,如数据库连接或子进程”。


但是对于1个问题,似乎没有实现此功能。您可以像这样直接将生成器传递给parametrize

@pytest.mark.parametrize('data', data_gen)
def test_gen(data):
    ...

但是pytest会list()生成器-> RAM问题也仍然存在。

我还发现了一些github issues,而不是why pytest无法懒惰地处理生成器。这似乎是一个设计问题。因此,由于

,“无法正确地将生成器作为值来管理参数化”
  

“ pytest必须收集所有带有所有元数据的测试...   收集总是在测试运行之前进行。”

在这种情况下,也有一些引用hypothesisnose's yield-base tests。但是,如果您仍然想坚持使用pytest,可以采取一些解决方法:

  1. 如果您以某种方式知道生成的参数的数量,则可以执行以下操作:
import pytest

def get_data(N):
    for i in range(N):
        yield list(range(N))

N = 3000
data_gen = get_data(N)
@pytest.mark.parametrize('ind', range(N))
def test_yield(ind):
    data = next(data_gen)
    assert data

因此,在这里您可以对index进行参数设置(这不是很有用-仅指示必须执行的pytest执行次数)并在下次运行时生成数据。 您也可以将其包装到memory_profiler

Results (46.53s):
    3000 passed
Filename: run_test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     40.6 MiB     40.6 MiB   @profile
     6                             def to_profile():
     7     76.6 MiB     36.1 MiB       pytest.main(['test.py'])

与简单明了的对比:

@pytest.mark.parametrize('data', data_gen)
def test_yield(data):
    assert data

“吃掉”更多的内存:

Results (48.11s):
    3000 passed
Filename: run_test.py

Line #    Mem usage    Increment   Line Contents
================================================
     5     40.7 MiB     40.7 MiB   @profile
     6                             def to_profile():
     7    409.3 MiB    368.6 MiB       pytest.main(['test.py'])
  1. 如果您想同时对另一个参数进行测试参数化,则可以对上一个子句进行一些概括,如下所示:
data_gen = get_data(N)
@pytest.fixture(scope='module', params=len_of_gen_if_known)
def fix():
    huge_data_chunk = next(data_gen)
    return huge_data_chunk


@pytest.mark.parametrize('other_param', ['aaa', 'bbb'])
def test_one(fix, other_param):
    data = fix
    ...

因此,我们在module范围级别使用固定装置,以便“预设”我们的数据以进行参数化测试。请注意,您可以在此处添加另一个测试,它也将接收生成的数据。只需在test_two之后添加它即可:

@pytest.mark.parametrize('param2', [15, 'asdb', 1j])
def test_two(fix, param2):
    data = fix
    ...

注意:如果您不知道生成的数据数量,则可以使用此技巧:设置一些近似值(最好是比生成的测试计数高一点),如果标记以{{ 1}},当所有数据都已经生成时就会发生。

  1. 另一种可能性是使用Factories as fixtures。在这里,您将生成器嵌入夹具中,并在测试中StopIteration合格直到结束。但这是另一个缺点-pytest会将其视为单个测试(可能包含一堆检查),并且如果生成的数据之一失败将失败。换句话说,如果与参数化方法相比,可能无法访问所有的pytest统计信息/功能。

  2. 另一种方法是在循环中使用try,如下所示:

pytest.main()

答案 1 :(得分:1)

您可能会发现此解决方法很有用:

from datetime import datetime, timedelta
from time import sleep

import pytest


@pytest.mark.parametrize(
    'lazy_params',
    [
        lambda: (datetime.now() - timedelta(days=1), datetime.now()),
        lambda: (datetime.now(), datetime.now() + timedelta(days=1)),
    ],
)
def test_it(lazy_params):
    yesterday, today = lazy_params()

    print(f'\n{yesterday}\n{today}')
    sleep(1)

    assert yesterday < today

样本输出:

========================================================================= test session starts ==========================================================================
platform darwin -- Python 3.7.7, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/apizarro/tmp
collected 2 items                                                                                                                                                      

test_that.py::test_it[<lambda>0] 
2020-04-14 18:34:08.700531
2020-04-15 18:34:08.700550
PASSED
test_that.py::test_it[<lambda>1] 
2020-04-15 18:34:09.702914
2020-04-16 18:34:09.702919
PASSED

========================================================================== 2 passed in 2.02s ===========================================================================

答案 2 :(得分:1)

编辑:我的第一个反应是“这正是参数化夹具的用途”:功能范围夹具是一个懒惰值,恰好在执行测试节点之前通过参数化调用您可以根据需要预定义多个夹具(例如,从数据库密钥列表中选择)。

from pytest_cases import fixture_plus

@fixture_plus
def db():
    return <todo>

@fixture_plus
@pytest.mark.parametrize("key", [<list_of keys>])
def sample(db, key):
    return db.get(key)

def test_foo(sample):
    return sample

话虽这么说,在某些(罕见)情况下,您仍然需要参数化函数中的惰性值,并且您不希望这些参数成为参数化灯具的变体。对于这些情况,pytest-cases中现在也有一个lazy_value解决方案。有了它,您就可以在参数值中使用函数,并且只有在执行当前测试时才调用这些函数。

下面是显示两种编码样式的示例(将use_partial布尔arg切换为True以启用另一种选择)

from functools import partial
from random import random

import pytest
from pytest_cases import lazy_value

database = [random() for i in range(10)]

def get_param(i):
    return database[i]


def make_param_getter(i, use_partial=False):
    if use_partial:
        return partial(get_param, i)
    else:
        def _get_param():
            return database[i]

        return _get_param

many_lazy_parameters = (make_param_getter(i) for i in range(10))

@pytest.mark.parametrize('a', [lazy_value(f) for f in many_lazy_parameters])
def test_foo(a):
    print(a)

请注意,如果您想自定义测试ID,lazy_value也有一个id参数。默认值为使用功能__name__,对部分功能的支持为on the way

您可以用相同的方式对灯具进行参数设置,但请记住,您必须使用@fixture_plus而不是@pytest.fixture。有关详情,请参见pytest-cases documentation

我是pytest-cases的作者;)