在 Python 中寻找片状测试的根本原因

Hunt for root cause of flaky test in Python

有一个不稳定的测试,我们不知道根本原因是什么。

with pytest.raises(foo.geocoder.GeocodingFailed):
    foo.geocoder.geocode('not a valid place')

有时不会发生异常。

我查看了文档 how to handle failures,但这没有帮助。

如何跟踪 geocode() 以便在没有发生异常时看到跟踪?

我查看了标准库的trace模块。但是似乎没有简单的方法可以将跟踪作为字符串。

“踪迹”的意思是:在 gecode() 期间执行的所有行的踪迹。我希望看到带有缩进的方法调用和 returns 语句。我想忽略 Python 标准库中的行。

据我所知,像 pdb 这样的调试器在这里没有帮助,因为测试只有在 CI 中获得 运行 时才会失败,并且每月只有一两次。

trace 库没有帮助,因为它不能写入 stringStringIO 对象,它只能写入 real 个文件。

你可以做的是使用 sys.settrace() 并定义一个简单的函数,每次执行都会调用它。您将在此处找到文档:https://docs.python.org/3/library/sys.html#sys.settrace。 基本魔法是从 frame 对象中获取详细信息,记录在此处:https://docs.python.org/3/library/inspect.html.

给您一个想法的示例如下所示:

import sys


def tracer(frame, event, arg):
    print(frame.f_code.co_name, frame.f_code.co_filename, frame.f_lineno, event, arg)


def bad_function(param: int):
    if param == 20:
        raise RuntimeError(f'that failed for {param}')


sys.settrace(tracer)

bad_function(1)
bad_function(20)
bad_function(2)

应该很容易将该信息存储到字符串中以供进一步调查,或者在出现异常时处理异常。

您尝试过将 pytest-repeat--pdb 一起使用吗?

类似于 pytest test_geocode.py --count 10 --pdb。如果您的 CI 在 docker 容器中工作并且您可以连接到容器,这可能会起作用。

或者,Jenkins 允许您在失败时暂停,然后以这种方式进行调查,但由于它不稳定并且每月只发生一次或两次,然后从字面上添加 --pdb 标志作为 [= 的一部分31=] 这样您就可以随时连接。您需要一种方法来通知您它已进入此流程,否则构建可能会超时。但如果您密切关注您的构建,这可能不是问题。

此外,如果您还没有使用 pytest-html,可能值得整合以获得关于发生了什么(通过日志行)以及失败或仅使用

的一个很好的报告

pytest test_geocode.py --log-file=/path/to/log/file --log-file-level=DEBUG.

将 pytest html 报告和/或日志文件作为人工制品附加到失败的构建中将允许更好的调试。

trace 模块通过 trace.Trace class.
提供编程访问 在测试失败时,Trace class' 控制台输出可见。
并且它有覆盖率报告可以写在选定的路径上。

我排除 sys.base_exec_prefixsys.base_prefix 不跟踪 Python 库模块。

import pytest


def f(x):
    import random

    choice = random.choice((True, False))

    if choice:
        raise ValueError
    else:
        return x * 2


def trace(f, *args, **kwargs):
    import trace
    import sys

    tracer = trace.Trace(
        ignoredirs=(sys.base_exec_prefix, sys.base_prefix), timing=True,
    )
    ret = tracer.runfunc(f, *args, **kwargs)
    r = tracer.results()
    r.write_results(coverdir="/tmp/xx-trace")
    return ret


def test_f():
    with pytest.raises(ValueError):
        trace(f, 3)

覆盖率报告;
标有 >>>>>> 的行未执行,即未跟踪,带冒号的数字是执行计数。

>>>>>> import pytest
       
       
>>>>>> def f(x):
    1:     import random
       
    1:     choice = random.choice((True, False))
       
    1:     if choice:
>>>>>>         raise ValueError
           else:
    1:         return x * 2
       
       
>>>>>> def trace(f, *args, **kwargs):
>>>>>>     import trace
>>>>>>     import sys
       
>>>>>>     tracer = trace.Trace(
>>>>>>         ignoredirs=(sys.base_exec_prefix, sys.base_prefix), timing=True,
           )
>>>>>>     tracer.runfunc(f, *args, **kwargs)
>>>>>>     r = tracer.results()
>>>>>>     r.write_results(coverdir="/tmp/xx-trace")
       
       
>>>>>> def test_f():
>>>>>>     with pytest.raises(ValueError):
>>>>>>         trace(f, 3)