首页 > 解决方案 > 在运行时修改(或包装)pytest 函数

问题描述

我需要获取一个函数体并将其包装在一个 try-finally 中。try-finally 必须进入函数体。

一些背景知识:我有使用特定夹具的 pytest 函数,我们将其称为check. 该对象用于进行断言,同时在 plain 之上添加额外的功能assert。一种能力是将实际断言推迟到最后,这样测试就不必在失败时提前结束。因此,该check对象有一个必须在其上调用的拆卸/终结器函数,check.finish(). 在调用该函数之前,不会实际评估断言。

我想把.finish()电话放在夹具中,像这样

@pytest.fixture
def check():
    _check = ...
    yield _check
    _check.finish()

然而,这还不够好,因为 pytest 和其他测试基础设施将AssertionError在夹具拆解中抛出的解释为Error,与 test 不同Failure,这是我想保留的区别。

所以check.finish()调用必须在测试函数体中:

def test_something(check):
    check.that(some_variable, "==", some_value)
    check.finish()

我想减轻这个样板文件并确保check.finish()始终调用它,即使测试主体抛出异常也是如此。到目前为止,我最好的想法(听起来很老套)是用来inspect.getsource()抓取函数体并将其包装在 try-finally 中,以便在运行时像这样评估函数:

def test_something(check):
    try:
        check.that(some_variable, "==", some_value)
    finally:
        check.finish()

即使测试将像这样存储在存储库中

def test_something(check):
    check.that(some_variable, "==", some_value)

我对这个解决方案的尝试显示在这里,它不起作用,因为它在缩进枚举步骤中访问了越界的行号,我认为enumerate()这应该防止。

def print_source(source_lines):
    """for debugging, adds line numbers"""
    print("".join((f"{str(i).zfill(3)}|{line}" for i, line in enumerate(source_lines))))


def add_try_finally_check_finish(fn: Callable):
    indent = " " * 4
    source_lines, _ = inspect.getsourcelines(fn)
    print_source(source_lines)

    # find beginning of function
    # assume the line after a `):` is the first line
    line_number = 0
    for line_number, line in enumerate(source_lines):
        if "):" in line:
            break

    # add `try:`
    try_line_number = line_number + 1
    source_lines.insert(try_line_number, f"{indent}try:\n")

    print_source(source_lines)

    # continue iterating and indent everything
    for line_number, line in enumerate(source_lines, start=try_line_number):
        source_lines[line_number] = indent + line

    # add teardown
    teardown = [
        f"{indent}finally:\n",
        f"{indent*2}check.finish()\n",
    ]
    source_lines.extend(teardown)

    print_source(source_lines)

    return compile("".join(source_lines), inspect.getsourcefile(fn), mode="exec")


@pytest.fixture
def check(request):
    _check = ...
    request.function.__code__ = add_try_finally_check_finish(request.function)
    yield _check

或者,这可以用装饰器来完成吗?这比在运行时更改源代码更好,但是我不知道如何编写这样的装饰器并将check对象传递给它。我在这里找到了其他可以成功装饰 pytest 函数的答案,但没有一个也可以访问固定装置。

@always_finish
def test_something(check):
    check.that(some_variable, "==", some_value)

这里的核心要求是,从 pytest 的角度来看,check.finish()在测试本身而不是在拆卸期间调用,即使测试本身抛出异常,所以欢迎其他实现这一点的创造性解决方案。

标签: pythonpytest

解决方案


推荐阅读