web-dev-qa-db-fra.com

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

Est-il possible d'obtenir les résultats d'un test (c'est-à-dire si toutes les assertions ont réussi) dans une méthode tearDown () J'utilise des scripts Selenium, et j'aimerais créer des rapports depuis l'intérieur de tearDown (), mais je ne sais pas si cela est possible.

56
Joey Robert

CAVEAT: Je n'ai aucun moyen de revérifier la théorie suivante pour le moment, étant éloigné de la boîte de dev. Donc, cela peut être un coup dans le noir.

Peut-être pourriez-vous vérifier la valeur de retour de sys.exc_info() dans votre méthode tearDown (), si elle renvoie (None, None, None), vous savez que le scénario de test a réussi. Sinon, vous pourriez utiliser Tuple renvoyé pour interroger l'objet exception.

Voir sys.exc_info documentation.

Une autre approche plus explicite consiste à écrire un décorateur de méthode sur lequel vous pourrez vous appuyer sur toutes vos méthodes de scénario de test nécessitant ce traitement spécial. Ce décorateur peut intercepter les exceptions d'assertion et modifier en conséquence un état dans self, permettant à votre méthode tearDown de connaître les événements.

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

Si vous examinez l'implémentation de unittest.TestCase.run, vous pouvez voir que tous les résultats de test sont collectés dans l'objet de résultat (généralement une instance de unittest.TestResult) transmis en tant qu'argument. Aucun statut de résultat n'est laissé dans l'objet unittest.TestCase.

Donc, vous ne pouvez pas faire grand chose avec la méthode unittest.TestCase.tearDown à moins que vous ne cassiez sans merci le découplage élégant des cas de test et des résultats de test 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 __== '__main__':
    unittest.main()

EDIT: Cela fonctionne pour Python 2.6 - 3.3, .__ (modifié pour le nouveau Python ci-dessous ).

37
scoffey

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

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

Suite à la réponse d'amatellanes, si vous utilisez 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

dégoûtant, mais ça a l'air de marcher.

9
hwjp

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

Si vous souhaitez effectuer certaines actions en cas d'échec (comme générer une capture d'écran ), au lieu d'utiliser tearDown(), vous pouvez le faire 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.__= AssertionError.__name__
    return MyFailureException
8
kenorb

Voici une solution pour ceux d’entre nous qui ne sommes pas à l’aise avec des solutions qui reposent sur des éléments internes unittest:

Tout d'abord, nous créons un décorateur qui définira un indicateur sur l'instance TestCase pour déterminer si le scénario de test 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 repose sur le fait que unittest détecte les tests ayant échoué via Exceptions. Autant que je sache, la seule exception spéciale à traiter est unittest.SkipTest (ce qui n'indique pas un échec du test). Toutes les autres exceptions indiquent des échecs de test. Nous les marquons comme telles lorsqu'elles nous parviennent.

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 être vraiment ennuyant d'écrire ce décorateur tout le temps. Y a-t-il un moyen de simplifier? Oui il y a!* Nous pouvons écrire une métaclasse pour gérer l’application du décorateur 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 cela à notre sous-classe TestCase de base 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 que cela ne gère pas correctement. Par exemple, il ne détecte pas correctement les échecs les sous-tests ni les échecs attendus. Je serais intéressé par d’autres modes d’échec, alors si vous trouvez un cas que je ne gère pas correctement, faites-le-moi savoir dans les commentaires et j’y reviendrai.


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

3
mgilson

Le nom du test en cours peut être récupéré avec la méthode unittest.TestCase.id () . Donc, dans tearDown, vous pouvez vérifier self.id (). 

L'exemple montre comment: 

  • rechercher si le test en cours comporte une erreur ou un échec dans la liste des erreurs ou des échecs
  • impression test id avec PASS ou FAIL ou EXCEPTION

L'exemple testé ici fonctionne avec l'exemple de @scoffey 'Nice. 

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
gaoithe

Python 2.7.

Vous pouvez également obtenir un résultat après unittest.main ():

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

ou utilisez suite:

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

Testé pour python 3.7 - exemple de code permettant d’obtenir des informations sur les assertions qui échouent, mais peut donner une idée de la gestion des erreurs:

def tearDown(self):
    if self._outcome.errors[1][1] and hasattr(self._outcome.errors[1][1][1], 'actual'):
        print(self._testMethodName)
        print(self._outcome.errors[1][1][1].actual)
        print(self._outcome.errors[1][1][1].expected)
0
pbaranski