使用 pytest 进行延迟参数化

max*_*zig 6 python performance pytest python-3.x

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

这是在 2 种情况下的问题:

  1. 当您有许多参数值(例如来自生成器)时 - 生成器和测试本身可能运行得很快,但所有这些参数值都会占用所有内存
  2. 当参数化具有不同类型的昂贵资源的装置时,您只能同时运行一个资源(例如,因为它们在同一个端口或类似的东西上侦听)

因此我的问题是:是否有可能告诉 pytest 即时评估参数(即懒惰)?

Bef*_*ght 4

至于你的第二个问题——在手册的评论链接中提出的问题似乎正是人们应该做的。它允许“仅在实际测试运行时设置昂贵的资源,例如数据库连接或子进程”。


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

@pytest.mark.parametrize('data', data_gen)
def test_gen(data):
    ...
Run Code Online (Sandbox Code Playgroud)

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

我还发现了一些 github问题,而不是进一步阐明为什么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)
Run Code Online (Sandbox Code Playgroud)
@pytest.mark.parametrize('ind', range(N))
def test_yield(ind):
    data = next(data_gen)
    assert data
Run Code Online (Sandbox Code Playgroud)

因此,在这里您进行参数化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'])
Run Code Online (Sandbox Code Playgroud)

并与直接比较:

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'])
Run Code Online (Sandbox Code Playgroud)

哪个“吃掉”更多的内存:

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'])
Run Code Online (Sandbox Code Playgroud)
  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
    ...
Run Code Online (Sandbox Code Playgroud)

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

@pytest.mark.parametrize('data', data_gen)
def test_yield(data):
    assert data
Run Code Online (Sandbox Code Playgroud)

注意:如果您不知道生成的数据的数量,您可以使用这个技巧:设置一些近似值(如果它比生成的测试计数高一点更好),并且如果它停止,则“标记”测试通过,StopIteration当所有数据已经生成。

  1. 另一种可能性是使用工厂作为固定装置。在这里,您将生成器嵌入到夹具中并try在测试中进行产量,直到它结束。但这里有另一个缺点 - pytest 会将其视为单个测试(内部可能有一堆检查),并且如果生成的数据之一失败,则会失败。换句话说,如果与参数化方法相比,并非所有 pytest 统计数据/功能都可以访问。

  2. 另一种是pytest.main()在循环中使用类似这样的东西:

# data_generate
# set_up test
pytest.main(['test'])
Run Code Online (Sandbox Code Playgroud)
  1. 与迭代器本身无关,而是在进行参数化测试时节省更多时间/RAM 的方法:只需在测试中移动一些参数化即可。例子:
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'])
Run Code Online (Sandbox Code Playgroud)

改成:

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
    ...
Run Code Online (Sandbox Code Playgroud)

它与工厂类似,但更容易实现。此外,它不仅多次减少 RAM,还减少收集元信息的时间。这里的缺点 - 对于 pytest 来说,这将是对所有two值的一次测试。它可以顺利地进行“简单”测试 - 如果xmark内部有一些特殊的东西或其他东西可能会出现问题。

我还打开了相应的问题,可能会出现有关此问题的一些附加信息/调整。