如何在 py.test 终结器中打印内容

How to print stuff in a py.test finalizer

我正在测试一个写入日志文件的函数(它专门写入日志文件并不重要,它可以做任何事情,这正是引起这个问题的原因)

像这样:

def do_stuff():
    with open('/tmp/mylogs.txt', 'a') as f:
        f.write(str(time.time()))
        f.write(' stuff done! \n')
    return 42

我可以这样测试一下:

def test_doing_stuff(watch_logs):
    assert do_stuff() == 42
    assert do_stuff() == 43

出于调试目的,当测试失败时,我希望能够打印出日志的新内容——我可以像这样制作固定装置:

@pytest.fixture()
def watch_logs(request):
    with open('/tmp/mylogs.txt') as f:
        log_before = f.read()

    def get_new_logs():
        with open('/tmp/mylogs.txt') as f:
            log_after = f.read()
            return log_after.replace(log_before, '')

    return get_new_logs

太好了 - 现在我可以在测试中随时查看日志内容:

def test_doing_stuff(watch_logs):
    assert do_stuff() == 42
    print(watch_logs())
    assert do_stuff() == 43
    print(watch_logs())

嗯 - 啊,但是第二次打印是行不通的,它是在测试失败之后。

如果我的测试夹具总是在测试结束时打印日志怎么办?然后 pytest 的 stdout 捕获会在失败时向我显示,但在通过时不会显示!

@pytest.fixture()
def watch_logs(request):
    with open('/tmp/mylogs.txt') as f:
        log_before = f.read()

    def get_new_logs():
        with open('/tmp/mylogs.txt') as f:
            log_after = f.read()
            return log_after.replace(log_before, '')

    def print_new_logs():
        print('~' * 20 + ' logs ' + '~' * 20)
        print(get_new_logs())
        print('~' * 50)
    request.addfinalizer(print_new_logs)

    return get_new_logs

哦,但这不起作用,因为在测试终结器期间没有发生 pytests 的日志捕获。

所以问题是:如何制作可以打印内容的测试终结器?

这是一个极简主义的要点,没有(不相关的)写入日志文件的内容:https://gist.github.com/hjwp/5154ec40a476a5c01ba6

没有记录或干净的方法来实现它,但这里有一个技巧:

# conftest.py

def pytest_runtest_call(item):
    if hasattr(item, "_request"):
        if hasattr(item._request, "_addoutput_on_failure"):
            item._request._addoutput_on_failure()

# test_x.py
import pytest

@pytest.fixture
def print_on_fail(request):
    def add():
        print ("test failed")
    request._parent_request._addoutput_on_failure = add

def test_doing_stuff(print_on_fail):
    assert False

我们可以考虑一个合适的request.addcall_on_failure(callback) API.

使 yield_fixture 案例工作需要一些内部可能不平凡的重构。

感谢 Holger 本人的帮助(感谢 @hpk42!),我得到了一些有用的东西。只是稍微 magic/hacky.

解决方案是使用一个名为 pytest_pyfunc_call 的 py.test 挂钩,以及一个名为 hookwrapper 的装饰器。他们为我提供了一种在测试运行之前和之后挂接一些代码的方法,而且还不受 stdout 劫持的影响。

我们在conftest.py中定义了一个新函数:

# conftest.py

@pytest.mark.hookwrapper
def pytest_pyfunc_call(pyfuncitem):
    yield
    print('this happens after the test runs')
    if 'watch_logs' in pyfuncitem.funcargs:
        print(pyfuncitem.funcargs['watch_logs']())

现在,如果 pytest 发现任何使用 watch_logs 夹具的测试,它将在测试运行后打印其输出。

完整示例:https://gist.github.com/hjwp/4294f0acbef5345a7d46