在 tearDown() 方法中获取 Python 的单元测试结果

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/4414234/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-18 15:39:43  来源:igfitidea点击:

Getting Python's unittest results in a tearDown() method

pythonunit-testingnose

提问by Joey Robert

Is it possible to get the results of a test (i.e. whether all assertions have passed) in a tearDown() method? I'm running Selenium scripts, and I'd like to do some reporting from inside tearDown(), however I don't know if this is possible.

是否可以在 tearDown() 方法中获得测试结果(即是否所有断言都已通过)?我正在运行 Selenium 脚本,我想从 tearDown() 内部做一些报告,但是我不知道这是否可行。

采纳答案by Pavel Repin

CAVEAT: I have no way of double checking the following theory at the moment, being away from a dev box. So this may be a shot in the dark.

警告:我目前无法仔细检查以下理论,因为远离开发箱。所以这可能是黑暗中的一个镜头。

Perhaps you could check the return value of sys.exc_info()inside your tearDown() method, if it returns (None, None, None), you know the test case succeeded. Otherwise, you could use returned tuple to interrogate the exception object.

也许你可以检查一下sys.exc_info()你的 tearDown() 方法内部的返回值,如果它返回(None, None, None),你就知道测试用例成功了。否则,您可以使用返回的元组来查询异常对象。

See sys.exc_infodocumentation.

请参阅sys.exc_info文档。

Another more explicit approach is to write a method decorator that you could slap onto all your test case methods that require this special handling. This decorator can intercept assertion exceptions and based on that modify some state in selfallowing your tearDown method to learn what's up.

另一种更明确的方法是编写一个方法装饰器,您可以将其添加到所有需要这种特殊处理的测试用例方法上。这个装饰器可以拦截断言异常,并基于它修改一些状态,self让你的 tearDown 方法了解发生了什么。

@assertion_tracker
def test_foo(self):
    # some test logic

回答by scoffey

If you take a look at the implementation of unittest.TestCase.run, you can see that all test results are collected in the result object (typically a unittest.TestResultinstance) passed as argument. No result status is left in the unittest.TestCaseobject.

如果您查看 的实现unittest.TestCase.run,您可以看到所有测试结果都收集在unittest.TestResult作为参数传递的结果对象(通常是一个实例)中。对象中没有留下任何结果状态unittest.TestCase

So there isn't much you can do in the unittest.TestCase.tearDownmethod unless you mercilessly break the elegant decoupling of test cases and test results with something like this:

因此,unittest.TestCase.tearDown除非您使用以下内容无情地打破测试用例和测试结果的优雅解耦,否则您在该方法中无能为力:

import unittest

class MyTest(unittest.TestCase):

    currentResult = None # holds last result object passed to run method

    def setUp(self):
        pass

    def tearDown(self):
        ok = self.currentResult.wasSuccessful()
        errors = self.currentResult.errors
        failures = self.currentResult.failures
        print ' All tests passed so far!' if ok else \
                ' %d errors and %d failures so far' % \
                (len(errors), len(failures))

    def run(self, result=None):
        self.currentResult = result # remember result for use in tearDown
        unittest.TestCase.run(self, result) # call superclass run method

    def test_onePlusOneEqualsTwo(self):
        self.assertTrue(1 + 1 == 2) # succeeds

    def test_onePlusOneEqualsThree(self):
        self.assertTrue(1 + 1 == 3) # fails

    def test_onePlusNoneIsNone(self):
        self.assertTrue(1 + None is None) # raises TypeError

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

EDIT: This works for Python 2.6 - 3.3, (modified for new Python bellow).

编辑:这适用于 Python 2.6 - 3.3,(针对新的 Python波纹管进行了修改)。

回答by amatellanes

If you are using Python2 you can use the method _resultForDoCleanups. This method return a TextTestResultobject:

如果您使用的是 Python2,则可以使用该方法_resultForDoCleanups。此方法返回一个TextTestResult对象:

<unittest.runner.TextTestResult run=1 errors=0 failures=0>

<unittest.runner.TextTestResult run=1 errors=0 failures=0>

You can use this object to check the result of your tests:

您可以使用此对象来检查测试结果:

def tearDown(self):
    if self._resultForDoCleanups.failures:
        ...
    elif self._resultForDoCleanups.errors:
        ...
    else:
        #Success

If you are using Python3 you can use _outcomeForDoCleanups:

如果您使用的是 Python3,则可以使用_outcomeForDoCleanups

def tearDown(self):
    if not self._outcomeForDoCleanups.success:
        ...

回答by hwjp

Following on from amatellanes' answer, if you're on Python3.4, you can't use _outcomeForDoCleanups. Here's what I managed to hack together:

继 amatellanes 的回答之后,如果您使用的是 Python3.4,则不能使用_outcomeForDoCleanups. 这是我设法一起破解的内容:

def _test_has_failed(self):
    for method, error in self._outcome.errors:
        if error:
            return True
    return False

yucky, but it seems to work.

令人讨厌,但它似乎有效。

回答by kenorb

It depends what kind of reporting you'd like to produce.

这取决于您想要生成什么样的报告。

In case you'd like to do some actions on failure (such as generating a screenshots), instead of using tearDown(), you may achieve that by overriding failureException.

如果您想对失败执行一些操作(例如生成屏幕截图),而不是使用tearDown(),您可以通过覆盖failureException.

For example:

例如:

@property
def failureException(self):
    class MyFailureException(AssertionError):
        def __init__(self_, *args, **kwargs):
            screenshot_dir = 'reports/screenshots'
            if not os.path.exists(screenshot_dir):
                os.makedirs(screenshot_dir)
            self.driver.save_screenshot('{0}/{1}.png'.format(screenshot_dir, self.id()))
            return super(MyFailureException, self_).__init__(*args, **kwargs)
    MyFailureException.__name__ = AssertionError.__name__
    return MyFailureException

回答by junfx

Python 2.7.

蟒蛇 2.7。

You can also get result after unittest.main():

您还可以在 unittest.main() 之后获得结果:

t = unittest.main(exit=False)
print t.result

or use suite:

或使用套件:

suite.addTests(tests)
result = unittest.result.TestResult()
suite.run(result)
print result

回答by gaoithe

Name of current test can be retrieved with unittest.TestCase.id()method. So in tearDown you can check self.id().

可以使用unittest.TestCase.id()方法检索当前测试的名称。所以在tearDown中你可以检查self.id()。

Example shows how to:

示例显示了如何:

  • find if current test has error or failure in errors or failures list
  • print test id with PASS or FAIL or EXCEPTION
  • 在错误或失败列表中查找当前测试是否有错误或失败
  • 使用 PASS 或 FAIL 或 EXCEPTION 打印测试 ID

Tested example here works with @scoffey 's nice example.

此处经过测试的示例适用于 @scoffey 的好示例。

def tearDown(self):
    result = "PASS"
    #### find and show result for current test
    # I did not find any nicer/neater way of comparing self.id() with test id stored in errors or failures lists :-7
    id = str(self.id()).split('.')[-1]
    # id() e.g. tup[0]:<__main__.MyTest testMethod=test_onePlusNoneIsNone>
    #           str(tup[0]):"test_onePlusOneEqualsThree (__main__.MyTest)"
    #           str(self.id()) = __main__.MyTest.test_onePlusNoneIsNone
    for tup in self.currentResult.failures:
        if str(tup[0]).startswith(id):
            print ' test %s failure:%s' % (self.id(), tup[1])
            ## DO TEST FAIL ACTION HERE
            result = "FAIL"
    for tup in self.currentResult.errors:
        if str(tup[0]).startswith(id):
            print ' test %s error:%s' % (self.id(), tup[1])
            ## DO TEST EXCEPTION ACTION HERE
            result = "EXCEPTION"

    print "Test:%s Result:%s" % (self.id(), result)

example of result:

结果示例:

python run_scripts/tut2.py 2>&1 
E test __main__.MyTest.test_onePlusNoneIsNone error:Traceback (most recent call last):
  File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
    self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

Test:__main__.MyTest.test_onePlusNoneIsNone Result:EXCEPTION
F test __main__.MyTest.test_onePlusOneEqualsThree failure:Traceback (most recent call last):
  File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
    self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true

Test:__main__.MyTest.test_onePlusOneEqualsThree Result:FAIL
Test:__main__.MyTest.test_onePlusOneEqualsTwo Result:PASS
.
======================================================================
ERROR: test_onePlusNoneIsNone (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
    self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'

======================================================================
FAIL: test_onePlusOneEqualsThree (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
     self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1, errors=1)

回答by Mark G.

Inspired by scoffey's answer, I decided to take mercilessnesto the next level, and have come up with the following.

受到scoffey 回答的启发,我决定将mercilessnes提升到一个新的水平,并提出以下建议。

It works in both vanilla unittest, and also when run via nosetests, and also works in Python versions 2.7, 3.2, 3.3, and 3.4 (I did not specifically test 3.0, 3.1, or 3.5, as I don't have these installed at the moment, but if I read the source codecorrectly, it should work in 3.5 as well):

它适用于 vanilla 单元测试,也适用于通过鼻子测试运行时,也适用于 Python 版本 2.7、3.2、3.3 和 3.4(我没有专门测试 3.0、3.1 或 3.5,因为我没有在现在,但如果我正确阅读源代码,它也应该在 3.5 中工作):

#! /usr/bin/env python

from __future__ import unicode_literals
import logging
import os
import sys
import unittest


# Log file to see squawks during testing
formatter = logging.Formatter(fmt='%(levelname)-8s %(name)s: %(message)s')
log_file = os.path.splitext(os.path.abspath(__file__))[0] + '.log'
handler = logging.FileHandler(log_file)
handler.setFormatter(formatter)
logging.root.addHandler(handler)
logging.root.setLevel(logging.DEBUG)
log = logging.getLogger(__name__)


PY = tuple(sys.version_info)[:3]


class SmartTestCase(unittest.TestCase):

    """Knows its state (pass/fail/error) by the time its tearDown is called."""

    def run(self, result):
        # Store the result on the class so tearDown can behave appropriately
        self.result = result.result if hasattr(result, 'result') else result
        if PY >= (3, 4, 0):
            self._feedErrorsToResultEarly = self._feedErrorsToResult
            self._feedErrorsToResult = lambda *args, **kwargs: None  # no-op
        super(SmartTestCase, self).run(result)

    @property
    def errored(self):
        if (3, 0, 0) <= PY < (3, 4, 0):
            return bool(self._outcomeForDoCleanups.errors)
        return self.id() in [case.id() for case, _ in self.result.errors]

    @property
    def failed(self):
        if (3, 0, 0) <= PY < (3, 4, 0):
            return bool(self._outcomeForDoCleanups.failures)
        return self.id() in [case.id() for case, _ in self.result.failures]

    @property
    def passed(self):
        return not (self.errored or self.failed)

    def tearDown(self):
        if PY >= (3, 4, 0):
            self._feedErrorsToResultEarly(self.result, self._outcome.errors)


class TestClass(SmartTestCase):

    def test_1(self):
        self.assertTrue(True)

    def test_2(self):
        self.assertFalse(True)

    def test_3(self):
        self.assertFalse(False)

    def test_4(self):
        self.assertTrue(False)

    def test_5(self):
        self.assertHerp('Derp')

    def tearDown(self):
        super(TestClass, self).tearDown()
        log.critical('---- RUNNING {} ... -----'.format(self.id()))
        if self.errored:
            log.critical('----- ERRORED -----')
        elif self.failed:
            log.critical('----- FAILED -----')
        else:
            log.critical('----- PASSED -----')


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

When run with unittest:

运行时unittest

$ ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]

$ cat ./test.log
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----

When run with nosetests:

运行时nosetests

$ nosetests ./test.py -v
test_1 (test.TestClass) ... ok
test_2 (test.TestClass) ... FAIL
test_3 (test.TestClass) ... ok
test_4 (test.TestClass) ... FAIL
test_5 (test.TestClass) ... ERROR

$ cat ./test.log
CRITICAL test: ---- RUNNING test.TestClass.test_1 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_2 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_3 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_4 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_5 ... -----
CRITICAL test: ----- ERRORED -----

Background

背景

I startedwith this:

这个开始

class SmartTestCase(unittest.TestCase):

    """Knows its state (pass/fail/error) by the time its tearDown is called."""

    def run(self, result):
        # Store the result on the class so tearDown can behave appropriately
        self.result = result.result if hasattr(result, 'result') else result
        super(SmartTestCase, self).run(result)

    @property
    def errored(self):
        return self.id() in [case.id() for case, _ in self.result.errors]

    @property
    def failed(self):
        return self.id() in [case.id() for case, _ in self.result.failures]

    @property
    def passed(self):
        return not (self.errored or self.failed)

However, this only works in Python 2. In Python 3, up to and including 3.3, the control flow appears to have changed a bit: Python 3's unittest package processes resultsaftercalling each test's tearDown()method… this behavior can be confirmed if we simply add an extra line (or six) to our test class:

然而,这仅适用于 Python 2。在 Python 3 中,直到并包括 3.3,控制流程似乎发生了一些变化:Python 3 的 unittest 包调用每个测试的方法处理结果……如果我们简单地添加,则可以确认这种行为我们的测试类多出一行(或六行):tearDown()

@@ -63,6 +63,12 @@
             log.critical('----- FAILED -----')
         else:
             log.critical('----- PASSED -----')
+        log.warning(
+            'ERRORS THUS FAR:\n'
+            + '\n'.join(tc.id() for tc, _ in self.result.errors))
+        log.warning(
+            'FAILURES THUS FAR:\n'
+            + '\n'.join(tc.id() for tc, _ in self.result.failures))


 if __name__ == '__main__':

Then just re-run the tests:

然后只需重新运行测试:

$ python3.3 ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]

…and you will see that you get this as a result:

......你会看到你得到这个结果:

CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:

CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:

CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4

Now, compare the above to Python 2's output:

现在,将上面的内容与 Python 2 的输出进行比较:

CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:

CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING  __main__: ERRORS THUS FAR:

WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----
WARNING  __main__: ERRORS THUS FAR:
__main__.TestClass.test_5
WARNING  __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4

Since Python 3 processes errors/failures afterthe test is torn down, we can't readily infer the result of a test using result.errorsor result.failuresin every case. (I think it probably makes more sense architecturally to process a test's results aftertearing it down, however, it doesmake the perfectly valid use-case of following a different end-of-test procedure depending on a test's pass/fail status a bit harder to meet…)

由于 Python 3在测试被拆除处理错误/失败,我们无法轻易推断使用result.errorsresult.failures在每种情况下的测试结果。(我认为拆除测试处理测试结果架构上可能更有意义,但是,它确实使根据测试的通过/失败状态遵循不同的测试结束程序的完全有效用例更难见面……)

Therefore, instead of relying on the overall resultobject, instead we can reference _outcomeForDoCleanupsas othershave alreadymentioned, which contains the result object for the currently running test, and has the necessary errorsand failruesattributes, which we can use to infer a test's status by the time tearDown()has been called:

因此,与其依靠整体的result对象,而不是我们可以引用_outcomeForDoCleanups别人已经提到的,它包含了当前正在运行的测试结果对象,并且有必要errorsfailrues属性,我们可以使用的时间来推断测试的状态tearDown()被称为:

@@ -3,6 +3,7 @@
 from __future__ import unicode_literals
 import logging
 import os
+import sys
 import unittest


@@ -16,6 +17,9 @@
 log = logging.getLogger(__name__)


+PY = tuple(sys.version_info)[:3]
+
+
 class SmartTestCase(unittest.TestCase):

     """Knows its state (pass/fail/error) by the time its tearDown is called."""
@@ -27,10 +31,14 @@

     @property
     def errored(self):
+        if PY >= (3, 0, 0):
+            return bool(self._outcomeForDoCleanups.errors)
         return self.id() in [case.id() for case, _ in self.result.errors]

     @property
     def failed(self):
+        if PY >= (3, 0, 0):
+            return bool(self._outcomeForDoCleanups.failures)
         return self.id() in [case.id() for case, _ in self.result.failures]

     @property

This adds support for the early versions of Python 3.

这增加了对 Python 3 早期版本的支持。

As of Python 3.4, however, this private member variable no longer exists, and instead, a new (albeit alsoprivate) method was added: _feedErrorsToResult.

然而,从 Python 3.4 开始,这个私有成员变量不再存在,而是添加了一个新的(尽管也是私有的)方法:_feedErrorsToResult.

This means that for versions 3.4 (and later), if the need is great enough, one can — very hackishlyforceone's way in to make it all work again like it did in version 2…

这意味着对于版本 3.4(及更高版本),如果需求足够大,人们可以——非常黑客——强行进入,让它像在版本 2 中那样再次工作......

@@ -27,17 +27,20 @@
     def run(self, result):
         # Store the result on the class so tearDown can behave appropriately
         self.result = result.result if hasattr(result, 'result') else result
+        if PY >= (3, 4, 0):
+            self._feedErrorsToResultEarly = self._feedErrorsToResult
+            self._feedErrorsToResult = lambda *args, **kwargs: None  # no-op
         super(SmartTestCase, self).run(result)

     @property
     def errored(self):
-        if PY >= (3, 0, 0):
+        if (3, 0, 0) <= PY < (3, 4, 0):
             return bool(self._outcomeForDoCleanups.errors)
         return self.id() in [case.id() for case, _ in self.result.errors]

     @property
     def failed(self):
-        if PY >= (3, 0, 0):
+        if (3, 0, 0) <= PY < (3, 4, 0):
             return bool(self._outcomeForDoCleanups.failures)
         return self.id() in [case.id() for case, _ in self.result.failures]

@@ -45,6 +48,10 @@
     def passed(self):
         return not (self.errored or self.failed)

+    def tearDown(self):
+        if PY >= (3, 4, 0):
+            self._feedErrorsToResultEarly(self.result, self._outcome.errors)
+

 class TestClass(SmartTestCase):

@@ -64,6 +71,7 @@
         self.assertHerp('Derp')

     def tearDown(self):
+        super(TestClass, self).tearDown()
         log.critical('---- RUNNING {} ... -----'.format(self.id()))
         if self.errored:
             log.critical('----- ERRORED -----')

…provided,of course, all consumers of this class remember to super(…, self).tearDown()in their respective tearDownmethods…

当然,前提是该类的所有使用者都记得super(…, self).tearDown()在各自的tearDown方法中…

Disclaimer:Purely educational, don't try this at home, etc. etc. etc. I'm not particularly proud of this solution, but it seems to work well enough for the time being, and is the best I could hack up after fiddling for an hour or two on a Saturday afternoon…

免责声明:纯粹是教育性的,不要在家里尝试这个,等等等等。我对这个解决方案并不特别自豪,但它目前似乎工作得很好,并且是我之后可以破解的最好的在星期六下午摆弄一两个小时......

回答by hynekcer

This solution is for Pythonversions 2.7 to 3.7(the highest current version), without any decorators or other modification in any code before tearDown. Everything works according to the builtin classification of results. Also skipped tests or expectedFailureare recognized correctly. It evaluates the result of the current test, not a summary of all tests passed so far. Compatible also with pytest.

此解决方案适用于Python版本2.7 到 3.7(当前最高版本),在tearDown. 一切都根据结果的内置分类工作。也跳过测试或被expectedFailure正确识别。它评估当前测试的结果,而不是迄今为止通过的所有测试的摘要。也与pytest兼容。

import unittest

class MyTest(unittest.TestCase):
    def tearDown(self):
        if hasattr(self, '_outcome'):  # Python 3.4+
            result = self.defaultTestResult()  # these 2 methods have no side effects
            self._feedErrorsToResult(result, self._outcome.errors)
        else:  # Python 3.2 - 3.3 or 3.0 - 3.1 and 2.7
            result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups)
        error = self.list2reason(result.errors)
        failure = self.list2reason(result.failures)
        ok = not error and not failure

        # demo:   report short info immediately (not important)
        if not ok:
            typ, text = ('ERROR', error) if error else ('FAIL', failure)
            msg = [x for x in text.split('\n')[1:] if not x.startswith(' ')][0]
            print("\n%s: %s\n     %s" % (typ, self.id(), msg))

    def list2reason(self, exc_list):
        if exc_list and exc_list[-1][0] is self:
            return exc_list[-1][1]

    # DEMO tests
    def test_success(self):
        self.assertEqual(1, 1)

    def test_fail(self):
        self.assertEqual(2, 1)

    def test_error(self):
        self.assertEqual(1 / 0, 1)

Comments:Only one or zero exceptions (error or failure) need be reported because not more can be expected before tearDown. The package unittestexpects that a second exception can be raised by tearDown. Therefore the lists errorsand failurescan contain only one or zero elements together before tearDown. Lines after "demo" comment are reporting a short result.

评论:只需要报告一个或零个异常(错误或失败),因为在tearDown. 该包unittest期望tearDown 可以引发第二个异常。因此,列表errors和列表failures在拆卸之前只能包含一个或零个元素。“演示”评论后的行报告了一个简短的结果。

Demo output:(not important)

演示输出:(不重要)

$ python3.5 -m unittest test

EF.
ERROR: test.MyTest.test_error
     ZeroDivisionError: division by zero
FAIL: test.MyTest.test_fail
     AssertionError: 2 != 1

==========================================================
... skipped usual output from unittest with tracebacks ...
...
Ran 3 tests in 0.002s

FAILED (failures=1, errors=1)


Comparision to other solutions- (with respect to commit history of Python source repository):

与其他解决方案的比较- (关于 Python 源代码库的提交历史):

  • This solutionuses a private attributeof TestCase instance like many other solutions, but I checked carefully all relevant commits in the Python source repository that three alternative names cover the code history since Python 2.7 to 3.6.2 without any gap. It can be a problem after some new major Python release, but it could be clearly recognized, skipped and easily fixed later for a new Python. An advantage is that nothing is modified before running tearDown, it should never break the test and all functionality of unittest is supported, works with pytest and it could work many extending packages, but not with nosetest (not a suprise becase nosetest is not compatible e.g. with unittest.expectedFailure).

  • The solutions with decoratorson the user test methods or with a customized failureException(mgilson, Pavel Repin 2nd way, kenorb) are robust against future Python versions, but if everything should work completely, they would grow like a snow ball with more supported exceptions and more replicated internals of unittest. The decorated functions have less readable tracebacks (even more levels added by one decorator), they are more complicated for debugging and it is unpleassant if another more important decorator has a problem. (Thanks to mgilson the basic functionality is ready and known issues can be fixed.)

  • The solution with modifired runmethod and catched resultparameter

    • (scoffey) should work also for Python 2.6. The interpretation of results can be improved to requirements of the question, but nothing can work in Python 3.4+, because resultis updated after tearDown call, never before.
    • Mark G.: (tested with Python 2.7, 3.2, 3.3, 3.4 and with nosetest)
  • solution by exc_info()(Pavel Repin 2st way) works only with Python 2.

  • Other solutions are principially similar, but less complete or with more disadvantages.

  • 这个解决方案像许多其他解决方案一样使用TestCase 实例的私有属性,但我仔细检查了 Python 源存储库中的所有相关提交,三个替代名称涵盖了自 Python 2.7 到 3.6.2 以来的代码历史,没有任何差距。在一些新的主要 Python 版本之后,这可能是一个问题,但可以清楚地识别、跳过并在以后轻松修复新的 Python。一个优点是在运行 tearDown 之前没有任何修改,它永远不会破坏测试并且支持 unittest 的所有功能,与 pytest 一起工作,它可以工作许多扩展包,但不能与鼻子测试一起工作(这并不奇怪,因为鼻子测试不兼容,例如与 unittest.expectedFailure)。

  • 在用户测试方法上带有装饰器或带有自定义 failureException( mgilson, Pavel Repin 2nd way, kenorb) 的解决方案对未来的 Python 版本是健壮的,但如果一切都完全正常,它们将像雪球一样增长,并提供更多支持的异常和单元测试的更多复制内部结构。被装饰的函数具有较少可读的回溯(一个装饰器添加的级别甚至更多),它们调试起来更复杂,如果另一个更重要的装饰器出现问题,这会令人不快。(感谢 mgilson,基本功能已准备就绪,已知问题可以修复。)

  • 修改run方法和捕获result参数的解决方案

    • ( scoffey) 也适用于 Python 2.6。结果的解释可以根据问题的要求进行改进,但在 Python 3.4+ 中没有任何效果,因为result在 tearDown 调用之后更新,之前从未更新过。
    • Mark G.:(用 Python 2.7、3.2、3.3、3.4 和鼻子测试测试)
  • exc_info()(Pavel Repin 2st way) 的解决方案仅适用于 Python 2。

  • 其他解决方案基本相似,但不完整或有更多缺点。



Explained by Python source repository
= Lib/unittest/case.py=
Python v 2.7 - 3.3

Python 源代码库解释
= Lib/unittest/case.py=
Python v 2.7 - 3.3

class TestCase(object):
    ...
    def run(self, result=None):
        ...
        self._outcomeForDoCleanups = result   # Python 3.2, 3.3
        # self._resultForDoCleanups = result  # Python 2.7
        #                                     # Python 2.6 - no result saved
        ...
        try:
            testMethod()
        except...   # many times for different exception classes
            result.add...(self, sys.exc_info())  # _addSkip, addError, addFailure
        ...
        try:
            self.tearDown()
        ...

Python v. 3.4 - 3.6

Python v. 3.4 - 3.6

    def run(self, result=None):
        ...
        # outocome is a context manager to catch and collect different exceptions
        self._outcome = outcome  
        ...
        with outcome...(self):
            testMethod()
        ...
        with outcome...(self): 
            self.tearDown() 
        ... 
        self._feedErrorsToResult(result, outcome.errors)

Note(by reading Python commit messages): A reasonwhy test results are so much decoupled from tests is memory leaksprevention. Every exception info can access to frames of the failed process state including all local variables. If a frame is assigned to a local variable in a code block that could also fail, then a cross memory refence could be easily created. It is not terrible, thanks to garbage collector, but the free memory can became fragmented more quickly than if the memory would be released correctly. This is a reason why exception information and traceback are converted very soon to strings and why temporary objects like self._outcomeare encapsulated and are set to None in a finallyblock in order to memory leaks are prevented.

注意(通过阅读 Python 提交消息):测试结果与测试如此分离的一个原因内存泄漏预防。每个异常信息都可以访问失败进程状态的帧,包括所有局部变量。如果将帧分配给代码块中的局部变量也可能失败,则可以轻松创建跨内存引用。多亏了垃圾收集器,这并不可怕,但是与正确释放内存相比,空闲内存可以更快地碎片化。这就是为什么异常信息和回溯很快被转换为字符串以及为什么临时对象self._outcome被封装并在finally块中设置为 None 以防止内存泄漏的原因。

回答by mgilson

Here's a solution for those of us who are uncomfortable using solutions that rely on unittestinternals:

对于我们这些不喜欢使用依赖unittest内部结构的解决方案的人来说,这是一个解决方案:

First, we create a decorator that will set a flag on the TestCaseinstance to determine whether or not the test case failed or passed:

首先,我们创建一个装饰器,它会在TestCase实例上设置一个标志,以确定测试用例是失败还是通过:

import unittest
import functools

def _tag_error(func):
    """Decorates a unittest test function to add failure information to the TestCase."""

    @functools.wraps(func)
    def decorator(self, *args, **kwargs):
        """Add failure information to `self` when `func` raises an exception."""
        self.test_failed = False
        try:
            func(self, *args, **kwargs)
        except unittest.SkipTest:
            raise
        except Exception:  # pylint: disable=broad-except
            self.test_failed = True
            raise  # re-raise the error with the original traceback.

    return decorator

This decorator is actually pretty simple. It relies on the fact that unittestdetects failed tests via Exceptions. As far as I'm aware, the only specialexception that needs to be handled is unittest.SkipTest(which does not indicate a test failure). All other exceptions indicate test failures so we mark them as such when they bubble up to us.

这个装饰器实际上非常简单。它依赖于unittest通过Exceptions检测失败的测试这一事实。据我所知,唯一需要处理的特殊异常是unittest.SkipTest(这并不表示测试失败)。所有其他异常都表示测试失败,因此当它们冒泡给我们时,我们将它们标记为此类。

We can now use this decorator directly:

我们现在可以直接使用这个装饰器:

class MyTest(unittest.TestCase):
    test_failed = False

    def tearDown(self):
        super(MyTest, self).tearDown()
        print(self.test_failed)

    @_tag_error
    def test_something(self):
        self.fail('Bummer')


It's going to get really annoying writing this decorator all the time. Is there a way we can simplify? Yes there is!*We can write a metaclass to handle applying the decorator for us:

一直写这个装饰器会变得非常烦人。有什么方法可以简化吗?就在这里!*我们可以编写一个元类来为我们处理应用装饰器:

class _TestFailedMeta(type):
    """Metaclass to decorate test methods to append error information to the TestCase instance."""
    def __new__(cls, name, bases, dct):
        for name, prop in dct.items():
            # assume that TestLoader.testMethodPrefix hasn't been messed with -- otherwise, we're hosed.
            if name.startswith('test') and callable(prop):
                dct[name] = _tag_error(prop)

        return super(_TestFailedMeta, cls).__new__(cls, name, bases, dct)

Now we apply this to our base TestCasesubclass and we're all set:

现在我们将它应用到我们的基本TestCase子类,我们都准备好了:

import six  # For python2.x/3.x compatibility

class BaseTestCase(six.with_metaclass(_TestFailedMeta, unittest.TestCase)):
    """Base class for all our other tests.

    We don't really need this, but it demonstrates that the
    metaclass gets applied to all subclasses too.
    """


class MyTest(BaseTestCase):

    def tearDown(self):
        super(MyTest, self).tearDown()
        print(self.test_failed)

    def test_something(self):
        self.fail('Bummer')

There are likely a number of cases that this doesn't handle properly. For example, it does not correctly detect failed subtestsor expected failures. I'd be interested in other failure modes of this, so if you find a case that I'm not handling properly, let me know in the comments and I'll look into it.

可能有许多情况无法正确处理。例如,它不能正确检测失败的子测试或预期的失败。我会对其他失败模式感兴趣,所以如果你发现我没有正确处理的情况,请在评论中告诉我,我会调查它。



*If there wasn't an easier way, I wouldn't have made _tag_errora private function ;-)

*如果没有更简单的方法,我就不会创建_tag_error私有函数;-)