当断言失败时继续进行Python的unit testing

编辑:切换到一个更好的例子,并澄清为什么这是一个真正的问题。

我想用Python编写unit testing,当断言失败时继续执行,以便在单个testing中看到多个失败。 例如:

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! 

在这里,testing的目的是确保Car的__init__正确设置其字段。 我可以把它分解成四个方法(这通常是一个好主意),但是在这种情况下,我认为将它作为testing单个概念的单一方法(“对象被正确初始化”)更具可读性。

如果我们假设在这里最好不要打破这个方法,那么我有一个新的问题:我不能一次看到所有的错误。 当我修复model错误并重新运行testing时,出现wheel_count错误。 当我第一次运行testing时,这会节省时间看到两个错误。

为了比较,Google的C ++unit testing框架区分了非致命的EXPECT_*断言和致命的ASSERT_*断言:

断言成对地testing相同的事物,但对当前函数有不同的影响。 ASSERT_ *版本失败时会生成致命故障,并中止当前的function。 EXPECT_ *版本会产生非致命的故障,不会中止当前的function。 通常,EXPECT_ *是首选,因为它们允许在testing中报告多个故障。 但是,如果在有问题的断言失败时无法继续,则应该使用ASSERT_ *。

有没有办法在Python的unit testing中获得类似EXPECT_*的行为? 如果不在unittest ,那么是否有另一个支持这种行为的Pythonunit testing框架?


顺便说一句,我很好奇多less实际testing可能从非致命的断言中受益,所以我查看了一些代码示例 (编辑2014-08-19使用search代码而不是谷歌代码search,RIP)。 在第一页的10个随机select的结果中,所有包含的testing在相同的testing方法中进行了多个独立的断言。 所有人都将受益于非致命的断言。

你可能想要做的就是派生unittest.TestCase因为这是断言失败时抛出的类。 你将不得不重新构build你的TestCase ,不要抛出(也许保留一个失败的列表)。 重新devise的东西可能会导致其他问题,你将不得不解决。 例如,您最终可能需要派生TestSuite来进行更改,以支持对TestCase所做的更改。

另一种具有非致命性断言的方法是捕获断言exception并将exception存储在列表中。 然后断言这个列表是空的,作为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() 

一个选项是一次将所有值断言为一个元组。

例如:

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

这个testing的输出是:

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

这表明模型和车轮数量都不正确。

在单个unit testing中有多个断言被认为是反模式。 预计一个unit testing只能testing一件事情。 也许你testing太多了。 考虑把这个testing分成多个testing。 这样你可以正确地命名每个testing。

然而,有时候,同时检查多个东西也是可以的。 例如,当你断言同一个对象的属性。 在这种情况下,你实际上是在断言该对象是否正确。 一种方法是编写一个自定义帮助器方法,知道如何在该对象上声明。 您可以编写该方法,使其显示所有失败的属性,或者在断言失败时显示期望对象的完整状态以及实际对象的完整状态。

每一个都用一个单独的方法来声明。

 class MathTest(unittest.TestCase): def test_addition1(self): self.assertEqual(1 + 0, 1) def test_addition2(self): self.assertEqual(1 + 1, 3) def test_addition3(self): self.assertEqual(1 + (-1), 0) def test_addition4(self): self.assertEqaul(-1 + (-1), -1) 

我喜欢@ Anthony-Batchelor的方法来捕获AssertionErrorexception。 但是,使用装饰器的方法略有不同,也是一种用pass / fail报告testing用例的方法。

 #!/usr/bin/env python # -*- coding: utf-8 -*- import unittest class UTReporter(object): ''' The UT Report class keeps track of tests cases that have been executed. ''' def __init__(self): self.testcases = [] print "init called" def add_testcase(self, testcase): self.testcases.append(testcase) def display_report(self): for tc in self.testcases: msg = "=============================" + "\n" + \ "Name: " + tc['name'] + "\n" + \ "Description: " + str(tc['description']) + "\n" + \ "Status: " + tc['status'] + "\n" print msg reporter = UTReporter() def assert_capture(*args, **kwargs): ''' The Decorator defines the override behavior. unit test functions decorated with this decorator, will ignore the Unittest AssertionError. Instead they will log the test case to the UTReporter. ''' def assert_decorator(func): def inner(*args, **kwargs): tc = {} tc['name'] = func.__name__ tc['description'] = func.__doc__ try: func(*args, **kwargs) tc['status'] = 'pass' except AssertionError: tc['status'] = 'fail' reporter.add_testcase(tc) return inner return assert_decorator class DecorateUt(unittest.TestCase): @assert_capture() def test_basic(self): x = 5 self.assertEqual(x, 4) @assert_capture() def test_basic_2(self): x = 4 self.assertEqual(x, 4) def main(): #unittest.main() suite = unittest.TestLoader().loadTestsFromTestCase(DecorateUt) unittest.TextTestRunner(verbosity=2).run(suite) reporter.display_report() if __name__ == '__main__': main() 

控制台输出:

 (awsenv)$ ./decorators.py init called test_basic (__main__.DecorateUt) ... ok test_basic_2 (__main__.DecorateUt) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK ============================= Name: test_basic Description: None Status: fail ============================= Name: test_basic_2 Description: None Status: pass 

期望在gtest中非常有用。 这是python方式的要点 ,代码:

 import sys import unittest class TestCase(unittest.TestCase): def run(self, result=None): if result is None: self.result = self.defaultTestResult() else: self.result = result return unittest.TestCase.run(self, result) def expect(self, val, msg=None): ''' Like TestCase.assert_, but doesn't halt the test. ''' try: self.assert_(val, msg) except: self.result.addFailure(self, sys.exc_info()) def expectEqual(self, first, second, msg=None): try: self.failUnlessEqual(first, second, msg) except: self.result.addFailure(self, sys.exc_info()) expect_equal = expectEqual assert_equal = unittest.TestCase.assertEqual assert_raises = unittest.TestCase.assertRaises test_main = unittest.main 

我不认为有办法用PyUnit来做到这一点,并不希望看到PyUnit以这种方式扩展。

我更喜欢坚持每个testing函数的一个断言( 或者更具体地说,每个testing声明一个概念 ),并将test_addition()重写为四个单独的testing函数。 这将提供更多关于失败的有用信息,

 .FF. ====================================================================== FAIL: test_addition_with_two_negatives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 10, in test_addition_with_two_negatives self.assertEqual(-1 + (-1), -1) AssertionError: -2 != -1 ====================================================================== FAIL: test_addition_with_two_positives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 6, in test_addition_with_two_positives self.assertEqual(1 + 1, 3) # Failure! AssertionError: 2 != 3 ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=2) 

如果你决定这种方法不适合你,你可能会发现这个答案有帮助。

更新

它看起来像你正在testing你的更新问题的两个概念,我会分裂成两个unit testing。 首先是参数被存储在创build一个新的对象上。 这将有两个断言,一个用于make ,一个用于model 。 如果第一次失败,显然需要修正,第二次通过或失败是无关紧要的。

第二个概念更可疑…你正在testing是否有一些默认值被初始化。 为什么 ? 在实际使用的时候testing这些值会更有用(如果它们没有被使用,那么为什么它们在那里呢?)。

这两个testing都失败了,都应该这样做。 当我是unit testing的时候,我对失败的兴趣远远超过我的成功,因为那是我需要集中注意力的地方。

 FF ====================================================================== FAIL: test_creation_defaults (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 25, in test_creation_defaults self.assertEqual(self.car.wheel_count, 4) # Failure! AssertionError: 3 != 4 ====================================================================== FAIL: test_creation_parameters (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 20, in test_creation_parameters self.assertEqual(self.car.model, self.model) # Failure! AssertionError: 'Ford' != 'Model T' ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=2)