在 Python 中伪造回溯

Ben*_*son 3 python testing unit-testing mocking traceback

我正在编写一个测试运行程序。我有一个可以捕获和存储异常的对象,稍后将其格式化为字符串作为测试失败报告的一部分。我正在尝试对格式化异常的过程进行单元测试。

在我的测试设置中,我不想实际抛出异常让我的对象捕获,主要是因为这意味着回溯将不可预测。(如果文件改变长度,回溯中的行号也会改变。)

如何将虚假回溯附加到异常,以便我可以断言它的格式化方式?这甚至可能吗?我正在使用 Python 3.3。

简化示例:

class ExceptionCatcher(object):
    def __init__(self, function_to_try):
        self.f = function_to_try
        self.exception = None
    def try_run(self):
        try:
            self.f()
        except Exception as e:
            self.exception = e

def format_exception_catcher(catcher):
    pass
    # No implementation yet - I'm doing TDD.
    # This'll probably use the 'traceback' module to stringify catcher.exception


class TestFormattingExceptions(unittest.TestCase):
    def test_formatting(self):
        catcher = ExceptionCatcher(None)
        catcher.exception = ValueError("Oh no")

        # do something to catcher.exception so that it has a traceback?

        output_str = format_exception_catcher(catcher)
        self.assertEquals(output_str,
"""Traceback (most recent call last):
  File "nonexistent_file.py", line 100, in nonexistent_function
    raise ValueError("Oh no")
ValueError: Oh no
""")
Run Code Online (Sandbox Code Playgroud)

Ben*_*son 5

阅读来源为traceback.py我指明了正确的方向。这是我的hacky解决方案,它涉及伪造回溯通常持有引用的框架和代码对象。

import traceback

class FakeCode(object):
    def __init__(self, co_filename, co_name):
        self.co_filename = co_filename
        self.co_name = co_name


class FakeFrame(object):
    def __init__(self, f_code, f_globals):
        self.f_code = f_code
        self.f_globals = f_globals


class FakeTraceback(object):
    def __init__(self, frames, line_nums):
        if len(frames) != len(line_nums):
            raise ValueError("Ya messed up!")
        self._frames = frames
        self._line_nums = line_nums
        self.tb_frame = frames[0]
        self.tb_lineno = line_nums[0]

    @property
    def tb_next(self):
        if len(self._frames) > 1:
            return FakeTraceback(self._frames[1:], self._line_nums[1:])


class FakeException(Exception):
    def __init__(self, *args, **kwargs):
        self._tb = None
        super().__init__(*args, **kwargs)

    @property
    def __traceback__(self):
        return self._tb

    @__traceback__.setter
    def __traceback__(self, value):
        self._tb = value

    def with_traceback(self, value):
        self._tb = value
        return self


code1 = FakeCode("made_up_filename.py", "non_existent_function")
code2 = FakeCode("another_non_existent_file.py", "another_non_existent_method")
frame1 = FakeFrame(code1, {})
frame2 = FakeFrame(code2, {})
tb = FakeTraceback([frame1, frame2], [1,3])
exc = FakeException("yo").with_traceback(tb)

print(''.join(traceback.format_exception(FakeException, exc, tb)))
# Traceback (most recent call last):
#   File "made_up_filename.py", line 1, in non_existent_function
#   File "another_non_existent_file.py", line 3, in another_non_existent_method
# FakeException: yo
Run Code Online (Sandbox Code Playgroud)

感谢@User 提供FakeException,这是必要的,因为真正的异常类型检查了 的参数with_traceback()

此版本确实有一些限制:

  • 它不会像真正的回溯那样打印每个堆栈帧的代码行,因为它format_exception会寻找代码来自的真实文件(在我们的例子中不存在)。如果你想让这个工作,你需要将假数据插入到 linecache的缓存中(因为traceback用于linecache获取源代码),根据@User 下面的回答

  • 你也不能真正提出 exc并期望假回溯能够存活下来。

  • 更一般地,如果您的客户端代码以与traceback(例如inspect 模块的大部分)不同的方式遍历回溯,则这些伪造可能不起作用。您需要添加客户端代码期望的任何额外属性。

这些限制对我来说很好——我只是将它用作调用代码的测试替身traceback——但如果你想做更多涉及的回溯操作,看起来你可能不得不深入到 C 级别。