如何在 TestCase 子类中隐藏堆栈帧?

问题描述 投票:0回答:2

我想向

TestCase
子类添加自定义断言方法。我尝试从
unittest
模块复制我的实现,以便它尽可能匹配常规
TestCase
的行为。 (我更愿意只委托给
self.assertEqual()
但这会导致更多的回溯噪音,见下文。)
unittest
模块似乎在报告失败的断言时自动隐藏其实现的一些内部细节。

import unittest

class MyTestCase(unittest.TestCase):
    def assertLengthIsOne(self, sequence, msg=None):
        if len(sequence) != 1:
            msg = self._formatMessage(msg, "length is not one")
            raise self.failureException(msg)

class TestFoo(MyTestCase):
    seq = (1, 2, 3, 4, 5)

    def test_stock_unittest_assertion(self):
        self.assertEqual(len(self.seq), 1)

    def test_custom_assertion(self):
        self.assertLengthIsOne(self.seq)


unittest.main()

其输出如下:

amoe@vuurvlieg $ python unittest-demo.py
FF
======================================================================
FAIL: test_custom_assertion (__main__.TestFoo)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest-demo.py", line 16, in test_custom_assertion
    self.assertLengthIsOne(self.seq)
  File "unittest-demo.py", line 7, in assertLengthIsOne
    raise self.failureException(msg)
AssertionError: length is not one

======================================================================
FAIL: test_stock_unittest_assertion (__main__.TestFoo)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "unittest-demo.py", line 13, in test_stock_unittest_assertion
    self.assertEqual(len(self.seq), 1)
AssertionError: 5 != 1

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=2)

请注意,自定义断言方法会导致堆栈跟踪具有两个框架,其中一个位于方法本身内部,而普通的

unittest
方法只有一个框架,即用户代码中的相关行。如何将这种框架隐藏行为应用到我自己的方法中?

python unit-testing testing subclass stack-trace
2个回答
17
投票

Peter Otten 在 comp.lang.python 上回答了这个问题

将 MyTestCase 移至单独的模块中并定义全局变量

__unittest = True

$ cat mytestcase.py import unittest __unittest = True class MyTestCase(unittest.TestCase): def assertLengthIsOne(self, sequence, msg=None): if len(sequence) != 1: msg = self._formatMessage(msg, "length is not one") raise self.failureException(msg) $ cat mytestcase_demo.py import unittest from mytestcase import MyTestCase class TestFoo(MyTestCase): seq = (1, 2, 3, 4, 5) def test_stock_unittest_assertion(self): self.assertEqual(len(self.seq), 1) def test_custom_assertion(self): self.assertLengthIsOne(self.seq) if __name__ == "__main__": unittest.main() $ python mytestcase_demo.py FF ====================================================================== FAIL: test_custom_assertion (__main__.TestFoo) ---------------------------------------------------------------------- Traceback (most recent call last): File "mytestcase_demo.py", line 11, in test_custom_assertion self.assertLengthIsOne(self.seq) AssertionError: length is not one ====================================================================== FAIL: test_stock_unittest_assertion (__main__.TestFoo) ---------------------------------------------------------------------- Traceback (most recent call last): File "mytestcase_demo.py", line 8, in test_stock_unittest_assertion self.assertEqual(len(self.seq), 1) AssertionError: 5 != 1 ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=2) $
    

0
投票
在 python 3.11 中,仍然没有公共 api 来隐藏堆栈框架中的辅助断言方法。设置

__unittest

 仍然有效,但我想更灵活地隐藏我的自定义断言方法。因此,我使用猴子补丁
TestResult._remove_unittest_tb_frames
从堆栈跟踪中删除assert_*和后续帧。

装饰器在包装函数内将变量设置为 True。我们创建的 Monkeypatched 方法会检查该变量,如果找到则截断当前和后续的 tb 节点。

有两个测试。一个带有

@helper_assert

 装饰器,另一个没有 

$ python -m unittest hide_assert_helpers before: test_with_verbose_stack -> assertTruthy2 -> nested_func after : test_with_verbose_stack -> assertTruthy2 -> nested_func Failure Traceback (most recent call last): File "hide_assert_helpers.py", line 80, in test_with_verbose_stack self.assertTruthy2("") File "hide_assert_helpers.py", line 64, in assertTruthy2 nested_func() File "hide_assert_helpers.py", line 62, in nested_func raise AssertionError("Value '{}' does not evaluate to True".format(val)) AssertionError: Value '' does not evaluate to True before: test_without_verbose_stack -> wrapper -> assertTruthy -> nested_func after : test_without_verbose_stack Failure Traceback (most recent call last): File "hide_assert_helpers.py", line 76, in test_without_verbose_stack self.assertTruthy("") AssertionError: Value '' does not evaluate to True
# hide_assert_helpers.py
from unittest import TestCase


# Remove helper assert methods from stack trace
def helper_assert(func):
    from functools import wraps
    from unittest import TestResult

    # print tb linked list
    def debug_print_linked_tb(tb, prefix=""):
        tb_list = []
        while tb:
            tb_list.append(tb.tb_frame.f_code.co_name)
            tb = tb.tb_next
        print(prefix + " -> ".join(tb_list))

    if not globals().get("_remove_unittest_tb_frames_setup"):
        globals()["_remove_unittest_tb_frames_setup"] = True
        _remove_unittest_tb_frames_copy = getattr(
            TestResult, "_remove_unittest_tb_frames"
        )

        def _remove_unittest_tb_frames_new(self_obj, tb):
            _remove_unittest_tb_frames_copy(self_obj, tb)

            debug_print_linked_tb(tb, prefix="before: ")

            tb_prev = None
            tb_curr = tb
            while tb_curr:
                is_decorator = tb_curr.tb_frame.f_locals.get(
                    "__unittest_helper_decorator"
                )

                # tb is a linked list
                # remove all nodes at and after @helper_assert decorator
                # we can technically splice middle frames and leave remaining if we want
                if is_decorator:
                    tb_prev.tb_next = None
                    break

                tb_prev = tb_curr
                tb_curr = tb_curr.tb_next

            debug_print_linked_tb(tb, prefix="after : ")

        setattr(
            TestResult, "_remove_unittest_tb_frames", _remove_unittest_tb_frames_new
        )

    @wraps(func)
    def wrapper(*args, **kwargs):
        __unittest_helper_decorator = True
        return func(*args, **kwargs)

    return wrapper


class StackOverflowTestCase(TestCase):
    def assertTruthy2(self, val):
        def nested_func():
            if not val:
                raise AssertionError("Value '{}' does not evaluate to True".format(val))

        nested_func()

    @helper_assert
    def assertTruthy(self, val):
        def nested_func():
            if not val:
                raise AssertionError("Value '{}' does not evaluate to True".format(val))

        nested_func()

    def test_without_verbose_stack(self):
        self.assertTruthy("foo")
        self.assertTruthy("")

    def test_with_verbose_stack(self):
        self.assertTruthy2("foo")
        self.assertTruthy2("")
    
© www.soinside.com 2019 - 2024. All rights reserved.