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

前端 未结 13 1331
野的像风
野的像风 2020-11-28 22:40

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 reporti

相关标签:
13条回答
  • 2020-11-28 23:02

    This solution is for Python versions 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 expectedFailure are recognized correctly. It evaluates the result of the current test, not a summary of all tests passed so far. Compatible also with 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 unittest expects that a second exception can be raised by tearDown. Therefore the lists errors and failures can contain only one or zero elements together before tearDown. Lines after "demo" comment are reporting a short result.

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

    • This solution uses a private attribute of 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 decorators on 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 run method and catched result parameter

      • (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 result is 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.


    Explained by Python source repository
    = 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

        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 reason why test results are so much decoupled from tests is memory leaks prevention. 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._outcome are encapsulated and are set to None in a finally block in order to memory leaks are prevented.

    0 讨论(0)
  • 2020-11-28 23:02

    In few words, this gives True if all test run so far exited with no errors or failures:

    class WatheverTestCase(TestCase):
    
        def tear_down(self):
            return not self._outcome.result.errors and not self._outcome.result.failures
    

    Explore _outcome's properties to access more detailed possibilities.

    0 讨论(0)
  • 2020-11-28 23:04

    Name of current test can be retrieved with unittest.TestCase.id() method. So in tearDown you can check 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

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

    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)
    
    0 讨论(0)
  • 2020-11-28 23:07

    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.

    See sys.exc_info documentation.

    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 self allowing your tearDown method to learn what's up.

    @assertion_tracker
    def test_foo(self):
        # some test logic
    
    0 讨论(0)
  • 2020-11-28 23:07

    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.TestResult instance) passed as argument. No result status is left in the unittest.TestCase object.

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

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

    0 讨论(0)
  • 2020-11-28 23:07

    I think the proper answer to your question is that there isn't a clean way to get test results in tearDown(). Most of the answers here involve accessing some private parts of the python unittest module and in general feel like workarounds. I'd strongly suggest avoiding these since the test results and test cases are decoupled and you should not work against that.

    If you are in love with clean code (like I am) I think what you should do instead is instantiating your TestRunner with your own TestResult class. Then you could add whatever reporting you wanted by overriding these methods:

    addError(test, err)
    Called when the test case test raises an unexpected exception. err is a tuple of the form returned by sys.exc_info(): (type, value, traceback).
    
    The default implementation appends a tuple (test, formatted_err) to the instance’s errors attribute, where formatted_err is a formatted traceback derived from err.
    
    addFailure(test, err)
    Called when the test case test signals a failure. err is a tuple of the form returned by sys.exc_info(): (type, value, traceback).
    
    The default implementation appends a tuple (test, formatted_err) to the instance’s failures attribute, where formatted_err is a formatted traceback derived from err.
    
    addSuccess(test)
    Called when the test case test succeeds.
    
    The default implementation does nothing.
    
    0 讨论(0)
提交回复
热议问题