当断言失败时继续Python的unittest

Bru*_*sen 74 python unit-testing

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

我想在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!
Run Code Online (Sandbox Code Playgroud)

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

如果我们假设这里最好不分解方法,那么我有一个新问题:我无法立即看到所有错误.当我修复model错误并重新运行测试时,会wheel_count出现错误.当我第一次运行测试时,它可以节省我看到两个错误的时间.

为了比较,Google的C++单元测试框架区分了非致命EXPECT_*断言和致命ASSERT_*断言:

断言成对出现,测试相同的东西但对当前函数有不同的影响.ASSERT_*版本在失败时会生成致命的故障,并中止当前的功能.EXPECT_*版本生成非致命故障,不会中止当前功能.通常EXPECT_*是首选,因为它们允许在测试中报告多个失败.但是,如果在有问题的断言失败时继续没有意义,则应使用ASSERT_*.

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


顺便说一下,我很好奇有多少现实测试可能会从非致命断言中受益,所以我看了一些代码示例(编辑2014-08-19使用searchcode代替Google Code Search,RIP).在第一页中随机选择的10个结果中,所有结果都包含在同一测试方法中进行多个独立断言的测试.所有人都将受益于非致命的断言.

Ant*_*lor 38

使用非致命断言的另一种方法是捕获断言异常并将异常存储在列表中.然后声明该列表是空的,作为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()
Run Code Online (Sandbox Code Playgroud)

  • 很确定我同意你的看法.这就是Selenium如何处理python后端中的验证错误. (2认同)

hwi*_*ers 27

一个选项是立即将所有值断言为元组.

例如:

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))
Run Code Online (Sandbox Code Playgroud)

此测试的输出将是:

======================================================================
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)
?           ^  ++++         ^
Run Code Online (Sandbox Code Playgroud)

这表明模型和车轮计数都不正确.


Zuk*_*uku 21

从 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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

  • 现在这应该是公认的答案,因为最容易放入现有代码中。 (2认同)

die*_*dha 8

您可能想要做的是派生,unittest.TestCase因为那是断言失败时抛出的类.您将不得不重新设计您TestCase的不投掷(可能会保留失败列表).重新构建东西可能会导致您必须解决的其他问题.例如,您可能最终需要派生TestSuite以进行更改以支持对您所做的更改TestCase.

  • 我会说为了实现软断言而覆盖 `TestCase` 是一种矫枉过正——它们在 python 中特别容易制作:只需捕获所有的 `AssertionError`s(可能在一个简单的循环中),并存储它们在一个列表或一组中,然后一次全部失败。查看@Anthony Batchelor 的答案以了解具体情况。 (4认同)
  • @dscordas取决于这是否用于一次性测试,或者您是否想在大多数测试中具有此功能。 (2认同)

Ste*_*ven 7

在单个单元测试中具有多个断言被认为是反模式.单个单元测试预计只测试一件事.也许你的测试太多了.考虑将此测试分成多个测试.这样您就可以正确命名每个测试.

但有时候,可以同时检查多个内容.例如,当您断言同一对象的属性时.在这种情况下,您实际上断言该对象是否正确.一种方法是编写一个自定义帮助器方法,该方法知道如何在该对象上断言.您可以编写该方法,使其显示所有失败的属性,或者例如在断言失败时显示预期对象的完整状态和实际对象的完整状态.

  • 他并没有说当你触及断言时测试不应该失败,他说失败不应该阻止其他检查.例如,现在我正在测试特定目录是用户,组和其他可写的.每个都是一个单独的断言.从测试输出中知道所有三种情况都失败是有用的,所以我可以用一个chmod调用修复它们,而不是得到"Path不是用户可写的",必须再次运行测试以获得"Path is不是群组可写的"等等.虽然我想我只是认为它们应该是单独的测试...... (10认同)
  • 任何原因导致测试的其余部分无法运行且仍然是致命的.我认为你可以在某个地方延迟失败的回归,转而支持聚合可能发生的所有可能的失败. (7认同)
  • 仅仅因为该库被称为unittest,并不意味着该测试是一个独立的单元测试.unittest模块,以及pytest和nose等,非常适合系统测试,集成测试等.但有一点需要注意,你只能失败一次.真的很烦人.我真的很想看到所有的断言函数要么添加一个允许你继续失败的参数,要么重复一个名为expectBlah的断言函数,它们会做这样的事情.然后用unittest编写更大的功能测试会更容易. (7认同)
  • 我想我们都说同样的话.我希望每个失败的断言都会导致测试失败; 只是我希望在测试方法返回时发生失败,而不是在测试断言时立即发生,如@dietbuddha所述.这将允许测试方法中的*all*断言,以便我可以一次性查看(并修复)所有失败.该测试仍然值得信赖,可读和可维护(实际上更是如此). (5认同)

ski*_*iou 6

PyPI 中有一个名为的软断言包softest可以满足您的要求。它的工作原理是收集故障、组合异常和堆栈跟踪数据,并将其全部报告为常规unittest输出的一部分。

例如,这段代码:

import softest

class ExampleTest(softest.TestCase):
    def test_example(self):
        # be sure to pass the assert method object, not a call to it
        self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
        # self.soft_assert(self.assertEqual('Worf', 'wharf', 'Klingon is not ship receptacle')) # will not work as desired
        self.soft_assert(self.assertTrue, True)
        self.soft_assert(self.assertTrue, False)

        self.assert_all()

if __name__ == '__main__':
    softest.main()
Run Code Online (Sandbox Code Playgroud)

...产生以下控制台输出:

======================================================================
FAIL: "test_example" (ExampleTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 14, in test_example
    self.assert_all()
  File "C:\...\softest\case.py", line 138, in assert_all
    self.fail(''.join(failure_output))
AssertionError: ++++ soft assert failure details follow below ++++

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The following 2 failures were found in "test_example" (ExampleTest):
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Failure 1 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 10, in test_example
    self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle')
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 829, in assertEqual
    assertion_func(first, second, msg=msg)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 1203, in assertMultiLineEqual
    self.fail(self._formatMessage(msg, standardMsg))
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 670, in fail
    raise self.failureException(msg)
AssertionError: 'Worf' != 'wharf'
- Worf
+ wharf
 : Klingon is not ship receptacle

+--------------------------------------------------------------------+
Failure 2 ("test_example" method)
+--------------------------------------------------------------------+
Traceback (most recent call last):
  File "C:\...\softest_test.py", line 12, in test_example
    self.soft_assert(self.assertTrue, False)
  File "C:\...\softest\case.py", line 84, in soft_assert
    assert_method(*arguments, **keywords)
  File "C:\...\Python\Python36-32\lib\unittest\case.py", line 682, in assertTrue
    raise self.failureException(msg)
AssertionError: False is not true


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)
Run Code Online (Sandbox Code Playgroud)

注意:我创建并维护softest


Len*_*bro 5

每个断言都用一个单独的方法.

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)
Run Code Online (Sandbox Code Playgroud)

  • 我意识到这是一种可能的解决方案,但它并不总是实用的.我正在寻找一些有用的东西,而不会将一个以前有凝聚力的测试分解成几个小方法. (4认同)