在Python的unittest中继续执行断言失败后的测试

98 投票
13 回答
66303 浏览
提问于 2025-04-16 10:13

编辑:换了一个更好的例子,并说明了为什么这是个真实的问题。

我想在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 个回答

34

一种选择是将所有的值一次性作为一个元组进行断言。

比如说:

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)
?           ^  ++++         ^

这表明模型和轮子的数量都是不正确的。

53

从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)
52

另一种实现非致命断言的方法是捕获断言异常,并把这些异常存储在一个列表里。然后在清理阶段(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()

撰写回答