断言失败时继续执行Python的单元测试


84

编辑:切换到一个更好的示例,并阐明了为什么这是一个真正的问题。

我想用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's__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个结果中,所有包含的测试都在同一测试方法中做出了多个独立的断言。所有人将从非致命的断言中受益。


2
你最终做了什么?我对此主题感兴趣(出于完全不同的原因,我很乐意在比评论更大的地方讨论这个问题),并希望了解您的经历。顺便说一句,“代码示例”链接的结尾处是“不幸的是,该服务已关闭”,因此,如果您有该版本的缓存版本,我也很乐意看到它。
戴维德

为了将来参考,我相信是在当前系统上进行的等效搜索,但是结果不再如上所述。
ZAD-Man 2014年

2
@Davide,我没做任何事情。对我来说,“每方法只声明一个”似乎过于僵化,但是唯一可行(且可维护)的解决方案似乎是安东尼的“捕获并追加”建议。但是,这对我来说太丑陋了,所以我只是在每个方法上坚持使用多个断言,而且我不得不忍受运行测试的次数要比发现所有失败所需的次数多。
布鲁斯·克里斯滕森

名为PyTest的python测试框架非常直观,默认情况下会显示所有断言失败。这可能是您所面临的问题的解决方法。
Surya Shekhar Chakraborty

Answers:


9

您可能想要做的是派生,unittest.TestCase因为这是断言失败时抛出的类。您将不得不重新架构您的架构TestCase,以免抛出错误(或者保留故障列表)。重新架构内容可能会导致其他必须解决的问题。例如,您可能最终需要进行派生TestSuite更改以支持对您所做的更改TestCase


1
我认为这可能是最终的答案,但是我想掩盖自己的基础,看看我是否遗漏了任何东西。谢谢!
布鲁斯·克里斯滕森

4
我想说,TestCase为了实现软断言而重写是一个过大的杀伤力-它们在python中特别容易制作:只需捕获所有AssertionErrors(也许在简单循环中),然后将它们存储在列表或集合中,然后立即全部失败。查看@Anthony Batchelor的答案以获取详细信息。
dcsordas

2
@dscordas取决于这是一次测试还是您想在大多数测试中具有此功能。
Dietbuddha 2013年

43

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

2
可以肯定,我同意你的看法。这就是Selenium处理python后端验证错误的方式。
Anthony Batchelor

是的,此解决方案的问题在于所有断言都被计为错误(不是故障转移),并且呈现错误的方式实际上并不可用。无论如何是一种方法,可以轻松地改善渲染功能
eMarine 2015年

我将此方法与Dietbudda的答案结合使用,通过覆盖unittest.TestCasetry / except块中的所有断言。
thodic

对于复杂的测试模式,这是克服unittest错误的最佳解决方案,但是对于所有try / excepts来说,它使测试看起来很难看。它是许多测试和复杂的单个测试之间的一个陷阱。我已经开始返回错误字典。因此,我可以在一项测试中测试整个测试模式,并为我的休闲python开发人员保持可读性。
MortenB

这是非常聪明的,所以向您致敬。
courtsimas

30

一个选择是对所有值立即声明为元组。

例如:

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

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


这很聪明。到目前为止,我找到的最好的解决方案。
陈妮

7

在单个单元测试中具有多个断言被认为是一种反模式。单个单元测试只能测试一件事。也许您正在测试太多。考虑将此测试分成多个测试。这样,您可以正确命名每个测试。

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


1
@Bruce:断言应该失败或成功。两者之间绝对不要有东西。测试应该是可信的,可读的和可维护的。未能通过测试的断言是一个坏主意。这会使您的测试过于复杂(从而降低了可读性和可维护性),而使测试“被允许失败”则很容易忽略它们,这意味着它们不可信。
史蒂文

8
其余测试无法运行的任何原因,仍然是致命的。我认为您可以将故障的返回推迟到某个位置,以便汇总可能发生的所有可能的故障。
Dietbuddha 2011年

5
我想我们俩都说同样的话。我希望每个失败的断言都会导致测试失败;只是我希望失败在测试方法返回时发生,而不是在测试断言时立即发生,如@dietbuddha所述。这将允许测试方法中的所有断言,以便一次查看所有错误(并修复)。该测试仍然是可信赖的,可读的和可维护的(实际上更是如此)。
布鲁斯·克里斯滕森

10
他不是说当您命中断言时测试就不会失败,而是说失败不应该阻止其他检查。例如,现在我正在测试特定目录是用户目录,组目录和其他可写目录。每个都是独立的断言。从测试输出中知道所有三种情况都将失败,因此我可以通过一个chmod调用来修复它们,而不是获取“ Path is not user-writable”,而不必再次运行测试以获取“ Path is user-writable”。不可组写”等等。尽管我想我只是争辩说应该对它们进行单独的测试……
蒂姆·基廷

8
仅仅因为该库称为单元测试,并不意味着该测试是一个隔离的单元测试。单元测试模块以及pytest,nose和其他模块,非常适合系统测试,集成测试等。需要注意的是,您只能失败一次。真烦人。我真的很想看到所有的断言函数要么添加一个允许您继续失败的参数,要么复制一个名为ExpectBlah的断言函数来执行此操作。然后,使用unittest编写较大的功能测试会更容易。
Okken 2014年

7

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

1
现在,这应该是最容易放入现有代码中的公认答案。
迈克尔·斯科特·库斯伯特

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)

5
我意识到这是一种可能的解决方案,但这并不总是可行的。我正在寻找一种有效的方法,而不会将以前的内聚性测试分解为几种小方法。
布鲁斯·克里斯滕森

@布鲁斯·克里斯滕森(Bruce Christensen):如果他们是如此凝聚力,那么也许他们会形成一个故事?然后可以将它们设置为doctest,即使在失败后,它们也继续存在。
Lennart Regebro

1
我有一组测试,如下所示:1.加载数据,2.断言数据正确加载,3.修改数据,4.断言修改正确,5.保存修改的数据,6.断言数据正确保存。我该怎么做呢?在中加载数据没有任何意义setup(),因为这是测试之一。但是,如果我将每个断言放到自己的函数中,那么我必须加载3次数据,这是对资源的巨大浪费。处理这种情况的最佳方法是什么?
naught101

好吧,测试特定序列的测试应该使用相同的测试方法。
Lennart Regebro

4

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

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

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

注意:我创建并维护softest


3

Expect在gtest中非常有用。这是gist中的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

2

我喜欢@ Anthony-Batchelor的方法来捕获AssertionError异常。但是使用装饰器的这种方法略有变化,并且还是通过/失败报告测试案例的方法。

#!/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

1

我对@Anthony Batchelor的回答有疑问,因为它会迫使我try...catch在单元测试中使用。相反,我将try...catch逻辑封装在TestCase.assertEqual方法的重写中。这是代码:

import unittest
import traceback

class AssertionErrorData(object):

    def __init__(self, stacktrace, message):
        super(AssertionErrorData, self).__init__()
        self.stacktrace = stacktrace
        self.message = message

class MultipleAssertionFailures(unittest.TestCase):

    def __init__(self, *args, **kwargs):
        self.verificationErrors = []
        super(MultipleAssertionFailures, self).__init__( *args, **kwargs )

    def tearDown(self):
        super(MultipleAssertionFailures, self).tearDown()

        if self.verificationErrors:
            index = 0
            errors = []

            for error in self.verificationErrors:
                index += 1
                errors.append( "%s\nAssertionError %s: %s" % ( 
                        error.stacktrace, index, error.message ) )

            self.fail( '\n\n' + "\n".join( errors ) )
            self.verificationErrors.clear()

    def assertEqual(self, goal, results, msg=None):

        try:
            super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg )

        except unittest.TestCase.failureException as error:
            goodtraces = self._goodStackTraces()
            self.verificationErrors.append( 
                    AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) )

    def _goodStackTraces(self):
        """
            Get only the relevant part of stacktrace.
        """
        stop = False
        found = False
        goodtraces = []

        # stacktrace = traceback.format_exc()
        # stacktrace = traceback.format_stack()
        stacktrace = traceback.extract_stack()

        # /programming/54499367/how-to-correctly-override-testcase
        for stack in stacktrace:
            filename = stack.filename

            if found and not stop and \
                    not filename.find( 'lib' ) < filename.find( 'unittest' ):
                stop = True

            if not found and filename.find( 'lib' ) < filename.find( 'unittest' ):
                found = True

            if stop and found:
                stackline = '  File "%s", line %s, in %s\n    %s' % ( 
                        stack.filename, stack.lineno, stack.name, stack.line )
                goodtraces.append( stackline )

        return goodtraces

# class DummyTestCase(unittest.TestCase):
class DummyTestCase(MultipleAssertionFailures):

    def setUp(self):
        self.maxDiff = None
        super(DummyTestCase, self).setUp()

    def tearDown(self):
        super(DummyTestCase, self).tearDown()

    def test_function_name(self):
        self.assertEqual( "var", "bar" )
        self.assertEqual( "1937", "511" )

if __name__ == '__main__':
    unittest.main()

结果输出:

F
======================================================================
FAIL: test_function_name (__main__.DummyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\User\Downloads\test.py", line 77, in tearDown
    super(DummyTestCase, self).tearDown()
  File "D:\User\Downloads\test.py", line 29, in tearDown
    self.fail( '\n\n' + "\n\n".join( errors ) )
AssertionError: 

  File "D:\User\Downloads\test.py", line 80, in test_function_name
    self.assertEqual( "var", "bar" )
AssertionError 1: 'var' != 'bar'
- var
? ^
+ bar
? ^
 : 

  File "D:\User\Downloads\test.py", line 81, in test_function_name
    self.assertEqual( "1937", "511" )
AssertionError 2: '1937' != '511'
- 1937
+ 511
 : 

可以在如何正确重写TestCase.assertEqual()并生成正确的stacktrace上发布更多有关正确的stacktrace捕获的替代解决方案


0

我不认为有办法用PyUnit做到这一点,也不想看到PyUnit以这种方式扩展。

我更喜欢对每个测试函数坚持一个断言(或更确切地说,对每个测试主张一个概念),并将其重写test_addition()为四个单独的测试函数。这将提供有关失败的更多有用信息,

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

如果您认为此方法不适合您,则可能会发现此答案很有帮助。

更新资料

看来您正在用更新的问题测试两个概念,我将把它们分为两个单元测试。首先是参数是在创建新对象时存储的。这将有两个断言,一个用于make和一个用于model。如果第一个失败,则显然需要解决该问题,而第二个通过或失败在此关头无关紧要。

第二个概念更为可疑...您正在测试是否初始化了一些默认值。为什么呢 在实际使用这些值时测试它们会更有用(如果不使用它们,那么为什么会出现这些值呢?)。

这些测试都失败了,并且都应该失败。当我进行单元测试时,我对失败的兴趣远胜于对成功的兴趣,因为这是我需要集中精力的地方。

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)

那么您会将Car.test_init分为四个功能吗?
布鲁斯·克里斯滕森

@布鲁斯·克里斯滕森(Bruce Christensen):我可能会将它分成两部分。但是即使那样,我也不确定您的断言是否有用。参见更新答案。
Johnsyweb 2011年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.