伪造Python中的追溯

时间:2013-10-08 13:17:55

标签: python unit-testing 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
""")

3 个答案:

答案 0 :(得分:3)

阅读the source of 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

感谢@User提供FakeException,这是必要的,因为真正的例外类型 - 检查with_traceback()的参数。

此版本有一些限制:

  • 它不会打印每个堆栈帧的代码行 追溯会,因为format_exception去寻找 代码来自的真实文件(在我们的例子中不存在)。 如果你想使这项工作,你需要插入假数据 linecache的 缓存(因为traceback使用linecache来获取源代码 代码),按@User's answer below

  • 您也无法提升 exc并期待假冒追溯 生存。

  • 更一般地说,如果你有遍历回溯的客户端代码 与traceback不同的方式(例如inspect的大部分内容 模块),这些假货可能不起作用。你需要添加任何东西 客户端代码期望的额外属性。

这些限制对于我的目的来说很好 - 我只是将它用作调用traceback的代码的测试双重 - 但是如果你想要做更多涉及回溯操作,它可能looks like你可能必须降到C级。

答案 1 :(得分:2)

EDIT2:

这是linecache的代码..我会评论它。

def updatecache(filename, module_globals=None): # module_globals is a dict
        # ...
    if module_globals and '__loader__' in module_globals:
        name = module_globals.get('__name__')
        loader = module_globals['__loader__']
            # module_globals = dict(__name__ = 'somename', __loader__ = loader)
        get_source = getattr(loader, 'get_source', None) 
            # loader must have a 'get_source' function that returns the source

        if name and get_source:
            try:
                data = get_source(name)
            except (ImportError, IOError):
                pass
            else:
                if data is None:
                    # No luck, the PEP302 loader cannot find the source
                    # for this module.
                    return []
                cache[filename] = (
                    len(data), None,
                    [line+'\n' for line in data.splitlines()], fullname
                )
                return cache[filename][2]

这意味着在你测试之前:

class Loader:
    def get_source(self):
        return 'source of the module'
import linecache
linecache.updatecache(filename, dict(__name__ = 'modulename without <> around', 
                                     __loader__ = Loader()))

'source of the module'是您测试的模块的来源。

EDIT1:

到目前为止我的解决方案:

class MyExeption(Exception):
    _traceback = None
    @property
    def __traceback__(self):
        return self._traceback
    @__traceback__.setter
    def __traceback__(self, value):
        self._traceback = value
    def with_traceback(self, tb_or_none):
        self.__traceback__ = tb_or_none
        return self

现在您可以设置例外的自定义回溯:

e = MyExeption().with_traceback(1)

如果你再加上例外,你通常会做什么:

raise e.with_traceback(fake_tb)

所有异常打印都会遍历此功能:

import traceback
traceback.print_exception(_type, _error, _traceback)

希望它有所帮助。

答案 2 :(得分:-1)

您应该能够在测试运行中简单地raise您想要的任何假异常。 python异常文档建议你创建一个类并将其作为例外。这是文档的第8.5节。

http://docs.python.org/2/tutorial/errors.html

一旦你创建了这个类,应该非常简单。