3

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

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

如何将假回溯附加到异常,以便我可以断言其格式化方式?这甚至可能吗?我正在使用 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
""")
4

3 回答 3

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

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

这个版本确实有一些限制:

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

  • 您也不能真正提出 exc并期望假追溯能够生存。

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

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

于 2013-10-08T21:23:41.960 回答
3

编辑2:

那是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'是您测试的模块的来源。

编辑1:

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

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)

希望它以某种方式有所帮助。

于 2013-10-08T19:35:46.623 回答
-1

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

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

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

于 2013-10-08T13:28:57.873 回答