我想向
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
方法只有一个框架,即用户代码中的相关行。如何将这种框架隐藏行为应用到我自己的方法中?
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)
$
__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("")