在Python的unittest中继续执行断言失败后的测试
编辑:换了一个更好的例子,并说明了为什么这是个真实的问题。
我想在Python中写单元测试,即使断言失败了也能继续执行,这样我就能在一个测试中看到多个失败的情况。例如:
class Car(object):
def __init__(self, make, model):
self.make = make
self.model = make # Copy and paste error: should be model.
self.has_seats = True
self.wheel_count = 3 # Typo: should be 4.
class CarTest(unittest.TestCase):
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
self.assertEqual(car.make, make)
self.assertEqual(car.model, model) # Failure!
self.assertTrue(car.has_seats)
self.assertEqual(car.wheel_count, 4) # Failure!
在这里,测试的目的是确保Car的__init__
方法能正确设置它的属性。我可以把它分成四个方法(这通常是个好主意),但在这种情况下,我觉得把它保持为一个方法更易读,因为它测试的是一个单一的概念(“对象被正确初始化”)。
如果我们假设这里最好不把方法拆开,那么我就遇到了一个新问题:我不能一次性看到所有的错误。当我修复了model
的错误并重新运行测试时,wheel_count
的错误又出现了。如果我能在第一次运行测试时看到这两个错误,那将节省我很多时间。
为了比较,谷歌的C++单元测试框架区分了非致命的EXPECT_*
断言和致命的ASSERT_*
断言:
这些断言成对出现,测试相同的内容,但对当前函数有不同的影响。
ASSERT_*
版本在失败时会产生致命错误,并终止当前函数。而EXPECT_*
版本则产生非致命错误,不会终止当前函数。通常情况下,EXPECT_*
更受欢迎,因为它允许在一个测试中报告多个失败。然而,如果在断言失败时继续执行没有意义,就应该使用ASSERT_*
。
在Python的unittest
中,有没有办法实现类似EXPECT_*
的行为?如果在unittest
中不行,那有没有其他支持这种行为的Python单元测试框架?
顺便说一下,我很好奇有多少实际测试可能会从非致命断言中受益,所以我查看了一些代码示例(2014-08-19编辑,使用searchcode代替Google代码搜索,怀念)。从第一页随机选出的10个结果中,所有的测试都在同一个测试方法中进行了多个独立的断言。所有这些测试都能从非致命断言中受益。
13 个回答
一种选择是将所有的值一次性作为一个元组进行断言。
比如说:
class CarTest(unittest.TestCase):
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
self.assertEqual(
(car.make, car.model, car.has_seats, car.wheel_count),
(make, model, True, 4))
这个测试的输出结果会是:
======================================================================
FAIL: test_init (test.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\temp\py_mult_assert\test.py", line 17, in test_init
(make, model, True, 4))
AssertionError: Tuples differ: ('Ford', 'Ford', True, 3) != ('Ford', 'Model T', True, 4)
First differing element 1:
Ford
Model T
- ('Ford', 'Ford', True, 3)
? ^ - ^
+ ('Ford', 'Model T', True, 4)
? ^ ++++ ^
这表明模型和轮子的数量都是不正确的。
从Python 3.4开始,你可以使用子测试功能:
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
with self.subTest(msg='Car.make check'):
self.assertEqual(car.make, make)
with self.subTest(msg='Car.model check'):
self.assertEqual(car.model, model)
with self.subTest(msg='Car.has_seats check'):
self.assertTrue(car.has_seats)
with self.subTest(msg='Car.wheel_count check'):
self.assertEqual(car.wheel_count, 4)
(msg
参数可以帮助你更轻松地找出哪个测试失败了。)
输出结果:
======================================================================
FAIL: test_init (__main__.CarTest) [Car.model check]
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 23, in test_init
self.assertEqual(car.model, model)
AssertionError: 'Ford' != 'Model T'
- Ford
+ Model T
======================================================================
FAIL: test_init (__main__.CarTest) [Car.wheel_count check]
----------------------------------------------------------------------
Traceback (most recent call last):
File "test.py", line 27, in test_init
self.assertEqual(car.wheel_count, 4)
AssertionError: 3 != 4
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=2)
另一种实现非致命断言的方法是捕获断言异常,并把这些异常存储在一个列表里。然后在清理阶段(tearDown)检查这个列表是否为空。
import unittest
class Car(object):
def __init__(self, make, model):
self.make = make
self.model = make # Copy and paste error: should be model.
self.has_seats = True
self.wheel_count = 3 # Typo: should be 4.
class CarTest(unittest.TestCase):
def setUp(self):
self.verificationErrors = []
def tearDown(self):
self.assertEqual([], self.verificationErrors)
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
try: self.assertEqual(car.make, make)
except AssertionError, e: self.verificationErrors.append(str(e))
try: self.assertEqual(car.model, model) # Failure!
except AssertionError, e: self.verificationErrors.append(str(e))
try: self.assertTrue(car.has_seats)
except AssertionError, e: self.verificationErrors.append(str(e))
try: self.assertEqual(car.wheel_count, 4) # Failure!
except AssertionError, e: self.verificationErrors.append(str(e))
if __name__ == "__main__":
unittest.main()