Obtenir les résultats unittest de Python dans une méthode tearDown ()

Est-il possible d'obtenir les résultats d'un test (c'est à dire si toutes les affirmations ont passé), dans un tearDown() la méthode? J'exécute des scripts de sélénium, et j'aimerais faire quelques reportages à partir de inside tearDown (), mais je ne sais pas si c'est possible.

49
demandé sur Joey Robert 2010-12-11 02:37:36

10 réponses

avertissement: je n'ai aucun moyen de revérifier la théorie suivante pour le moment, étant à l'écart d'une dev box. Donc c'est peut-être un tir dans le noir.

peut-être pourriez-vous vérifier la valeur de retour de sys.exc_info() à l'intérieur de votre méthode tearDown (), si elle retourne (None, None, None) , vous savez que le cas de test a réussi. Sinon, vous pouvez utiliser le tuple retourné pour interroger l'objet exception.

voir sys.exc_info de la documentation.

une autre approche plus explicite est d'écrire un décorateur de méthode que vous pourriez mettre sur toutes vos méthodes de cas de test qui exigent cette manipulation spéciale. Ce décorateur peut intercepter des exceptions d'assertion et sur la base de cela modifier un État dans self permettant à votre méthode de démontage pour apprendre ce qui se passe.

@assertion_tracker
def test_foo(self):
    # some test logic
15
répondu Pavel Repin 2010-12-11 02:14:32

si vous regardez l'implémentation de unittest.TestCase.run , vous pouvez voir que tous les résultats de test sont collectés dans l'objet résultat (typiquement une instance unittest.TestResult ) passé comme argument. Aucun statut de résultat n'est laissé dans l'objet unittest.TestCase .

il n'y a donc pas grand chose que vous puissiez faire dans la méthode unittest.TestCase.tearDown à moins que vous ne brisiez impitoyablement l'élégant découplage des cas d'essai et des résultats d'essai avec quelque chose comme ceci:

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: Cela fonctionne pour Python 2.6-3.3, (modifié pour le nouveau Python bellow ).

35
répondu scoffey 2017-05-23 12:02:54

Cette solution est pour Python versions 2.7 à 3.6 (la version la plus récente et le développement peu avant 3.7-alpha ), sans décorateurs ou autre modification dans n'importe quel code avant tearDown . Tout fonctionne selon la classification intégrée des résultats. Les tests expectedFailure ou "skipped tests" sont également reconnus correctement. Il évalue le résultat de l'examen, pas un résumé de tous les tests passé jusqu'à présent. Compatible aussi avec 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)

commentaires: une seule ou zéro exception (erreur ou échec) doit être déclarée, car on ne peut s'attendre à ce qu'il y en ait plus avant tearDown . Le paquet unittest prévoit qu'une deuxième exception peut être soulevée par démontage. Par conséquent, les listes errors et failures ne peuvent contenir ensemble qu'un ou zéro élément avant démontage. Lignes après "démo" les commentaires font état d'un court résultat.

Démo output: (Non 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)

Comparaison avec d'autres solutions - (par rapport à commettre l'histoire de Python référentiel source):

  • Cette solution utilise un attribut privé d'instance de TestCase comme plusieurs d'autres solutions, mais j'ai vérifié attentivement toutes les propagations pertinentes dans le dépôt source de Python que trois noms alternatifs couvrent l'histoire du code depuis Python 2.7 à 3.6.2 sans aucune discontinuité. Il peut être un problème après quelques de nouveaux grands Version Python, mais elle pourrait être clairement reconnue, sautée et facilement corrigée plus tard pour un nouveau Python. Un avantage est que rien n'est modifié avant en cours d'exécution démontage, il ne devrait jamais briser le test et toutes les fonctionnalités de unittest est supporté, fonctionne avec pytest, pourrait fonctionner avec de nombreux paquets prolongeant, mais pas avec nosetest (pas de surprise car nosetest n'est pas compatible par exemple avec unittest.expectedFailure).

  • les solutions avec décorateurs sur les méthodes d'essai de l'utilisateur ou avec un failureException ( mgilson , Pavel Repin 2nd way, kenorb) sont robustes contre avenir Python versions, mais si tout devrait fonctionner complètement, ils poussent comme une boule de neige avec plus d'exceptions supportées et plus d'internes répliqués de unittest. Décoré de fonctions ont moins lisible retraçage (encore plus de niveaux ajoutés par un décorateur), ils sont plus complexes pour Débogage et il est peu rassurant si un autre décorateur plus important a un problème. (Grâce à mgilson la fonctionnalité de base est prête et connue les problèmes peuvent être résolus.)

  • La solution avec modifired run méthode et enclenché result paramètre

    • ( scoffey ) devrait fonctionner aussi pour la version 2.6 de Python. L'interprétation des résultats peut être améliorée exigences de la question, mais rien ne peut fonctionner en Python 3.4+, parce que result est mis à jour après l'appel de larme, jamais avant.
    • Mark G.: (testé avec Python 2.7, 3.2, 3.3, 3.4 et avec nosetest)
  • solution by exc_info() (Pavel Repin 2st way) NE FONCTIONNE qu'avec Python 2.

  • D'autres solutions sont essentiellement similaires, mais moins complètes ou avec plus de inconvénient.


expliqué par le dépôt source 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

    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 (en lisant les messages de propagation): une des raisons pour lesquelles les résultats des tests sont tellement découplés des tests est fuites de mémoire prévention. Chaque information d'exception peut accéder aux cadres de l'état de processus échoué, y compris toutes les variables locales. Si une trame est assignée à une variable locale dans un code block qui pourrait aussi échouer, alors une référence de mémoire croisée pourrait être facilement créée. Ce n'est pas terrible, grâce à garbage collector, mais la mémoire peut fragmenté plus rapidement que si la mémoire serait libérée correctement. C'est la raison pour laquelle les informations d'exception et de retraçage sont converties très rapidement en chaînes et pourquoi les objets temporaires comme self._outcome sont encapsulés et placés à None dans un bloc finally afin d'éviter les fuites de mémoire.

21
répondu hynekcer 2017-08-31 18:22:54

Si vous utilisez Python2 vous pouvez utiliser la méthode _resultForDoCleanups . Cette méthode retourne un TextTestResult objet:

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

Vous pouvez utiliser cet objet pour vérifier le résultat de vos tests:

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

si vous utilisez Python3 vous pouvez utiliser _outcomeForDoCleanups :

def tearDown(self):
    if not self._outcomeForDoCleanups.success:
        ...
10
répondu amatellanes 2014-03-23 21:42:53

suite à la réponse d'amatellanes, si vous êtes sur Python3.4, vous ne pouvez pas utiliser _outcomeForDoCleanups . Voici ce que j'ai réussi à pirater ensemble:

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

yucky, mais ça semble marcher.

9
répondu hwjp 2014-04-19 22:38:35

cela dépend du type de rapport que vous souhaitez produire.

dans le cas où vous souhaitez faire certaines actions sur l'échec (comme générer des captures d'écran ), au lieu d'utiliser tearDown() , vous pouvez atteindre cet objectif en remplaçant failureException .

par exemple:

@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
8
répondu kenorb 2017-05-23 12:26:39

Voici une solution pour ceux d'entre nous qui sont mal à l'aise en utilisant des solutions qui s'appuient sur unittest internes:

tout d'abord, nous créons un décorateur qui placera un drapeau sur l'instance TestCase pour déterminer si oui ou non le cas d'essai a échoué ou réussi:

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

ce décorateur est en fait assez simple. Il s'appuie sur le fait que unittest détecte des essais ratés via Exceptions . Aussi loin que Je sais que la seule exception spéciale qui doit être traitée est unittest.SkipTest (ce qui n'indique pas un échec au test). Toutes les autres exceptions indiquent des échecs de test, donc nous les marquons comme tels quand ils se présentent à nous.

nous pouvons maintenant utiliser ce décorateur directement:

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

ça va devenir vraiment ennuyeux d'écrire tout le temps ce décorateur. Est-il un moyen que nous pouvons simplifier? Oui il y en a! * Nous pouvons écrire une métaclasse pour gérer l'application de la décoratrice pour nous:

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)

maintenant nous appliquons ceci à notre base TestCase sous-classe et nous sommes tous ensemble:

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

il y a probablement un certain nombre de CAs qui ne sont pas traités correctement. Par exemple, ne détecte pas correctement les échecs subtests ou les échecs attendus. Je serais intéressé par d'autres modes de défaillance de ce, donc si vous trouvez un cas que je ne gère pas correctement, faites-le moi savoir dans les commentaires et je vais regarder.


* S'il n'y avait pas un moyen plus simple, je n'aurais pas fait de _tag_error une fonction privée; -)

3
répondu mgilson 2017-08-31 12:59:07

Python 2.7.

vous pouvez également obtenir le résultat après unittest.main ():

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

ou utiliser la suite:

suite.addTests(tests)
result = unittest.result.TestResult()
suite.run(result)
print result
1
répondu junfx 2016-04-13 07:41:19

nom du test courant peut être récupéré avec unittest.TestCase.id () method. Donc, à tearDown, vous pouvez vérifier vous-même.id ().

exemple montre comment:

  • rechercher si les test a l'erreur ou l'échec dans les erreurs ou les défaillances de la liste
  • test d'impression id avec la réussite ou l'ÉCHEC ou de l'EXCEPTION

exemple testé ici fonctionne avec le bel exemple de @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)

exemple de résultat:

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)
1
répondu gaoithe 2016-06-13 12:35:14

inspiré par réponse de scoffey , j'ai décidé de prendre mercilessnes à un niveau supérieur, et ont trouvé ce qui suit.

il fonctionne à la fois en unittest vanilla, et aussi lorsqu'il est exécuté via nosetests, et fonctionne également dans les versions 2.7, 3.2, 3.3, et 3.4 de Python (je n'ai pas testé spécifiquement 3.0, 3.1, ou 3.5, car je ne les ai pas installés pour le moment, mais si je lis le code source correctement, il devrait fonctionner en 3.5 aussi bien):

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

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

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

arrière-plan

I a commencé avec ceci:

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)

cependant, cela ne fonctionne qu'en Python 2. Dans Python 3, jusqu'à 3.3 inclus, le flux de contrôle semble avoir changé un peu: Python Le paquet unittest de 3 traite les résultats après appelant la méthode de chaque test tearDown() ... ce comportement peut être confirmé si nous ajoutons simplement une ligne supplémentaire (ou six) à notre classe de test:

@@ -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__':

puis il suffit de relancer les 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
[…]

...et vous verrez que vous obtenez ce résultat:

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

maintenant, comparez ce qui précède à la sortie de 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

puisque Python 3 traite les erreurs /Échecs après le test est démoli, nous ne pouvons pas facilement inférer le résultat d'un test en utilisant result.errors ou result.failures dans chaque cas. (Je pense qu'il est probablement plus sensé sur le plan de l'architecture de traiter les résultats d'un test après le démonter, cependant, il ne faire le cas d'utilisation parfaitement valide de suivre une procédure de fin de test différente en fonction de l'état de réussite/échec d'un test un peu plus difficile à satisfaire...)

donc, au lieu de compter sur l'ensemble result objet, nous pouvons faire référence à _outcomeForDoCleanups comme autres ont déjà mentionné, qui contient l'objet de résultat pour le test en cours d'exécution, et a les nécessaires errors et failrues attributs, que nous pouvons utiliser pour inférer le statut d'un test par le temps tearDown() a a été appelé:

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

ceci ajoute le support pour les premières versions de Python 3.

à partir de Python 3.4, cependant, cette variable membre privé n'existe plus , et à la place, une nouvelle méthode (bien que également privé) a été ajoutée: _feedErrorsToResult .

cela signifie que pour les versions 3.4 ( et suivantes ), si le besoin est grand assez, on peut - très farfelu - forcez un moyen de faire en sorte que tout fonctionne à nouveau comme il l'a fait dans la version 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 -----')

... à condition, bien entendu, tous les consommateurs de cette classe se souviennent de super(…, self).tearDown() dans leurs tearDown méthodes ...

avertissement: purement éducatif, ne tentez pas cela à la maison, etc. etc. etc. Je ne suis pas particulièrement fier de cette solution, mais elle semble fonctionner assez bien pour le moment, et c'est la meilleure que j'ai pu pirater après avoir joué du violon pendant une heure ou deux un samedi après-midi...

1
répondu Mark G. 2017-05-23 12:34:57