unboundlocalerror - Poursuivre dans l'unittest de Python quand une assertion échoue




return python exception (6)

EDIT: passé à un meilleur exemple, et clarifié pourquoi c'est un vrai problème.

Je voudrais écrire des tests unitaires en Python qui continuent à s'exécuter quand une assertion échoue, de sorte que je puisse voir plusieurs échecs dans un seul test. Par exemple:

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!

Ici, le but du test est de s'assurer que __init__ de __init__ définit correctement ses champs. Je pourrais le décomposer en quatre méthodes (et c'est souvent une bonne idée), mais dans ce cas je pense qu'il est plus lisible de le garder comme une méthode unique qui teste un seul concept ("l'objet est initialisé correctement").

Si nous supposons qu'il est préférable ici de ne pas casser la méthode, alors j'ai un nouveau problème: je ne peux pas voir toutes les erreurs à la fois. Lorsque je wheel_count erreur du model et réexécute le test, l'erreur wheel_count apparaît. Cela me ferait gagner du temps pour voir les deux erreurs lorsque je passerai le test pour la première fois.

À titre de comparaison, le cadre de test unitaire C ++ de Google fait la distinction entre les assertions EXPECT_* non fatales et les assertions ASSERT_* fatales:

Les assertions viennent par paires qui testent la même chose mais ont des effets différents sur la fonction actuelle. Les versions ASSERT_ * génèrent des échecs fatals lorsqu'elles échouent et abandonnent la fonction en cours. Les versions EXPECT_ * génèrent des échecs non-fatals, qui n'annulent pas la fonction en cours. Habituellement, EXPECT_ * est préféré, car ils permettent de signaler plus d'un échec dans un test. Cependant, vous devriez utiliser ASSERT_ * si cela n'a pas de sens de continuer lorsque l'assertion en question échoue.

Existe-t-il un moyen d'obtenir EXPECT_* comportement semblable à EXPECT_* dans l' unittest de Python? Si ce n'est pas le cas, y a-t-il un autre framework de test d'unité Python qui supporte ce comportement?

Incidemment, j'étais curieux de savoir combien de tests réels pourraient bénéficier d'assertions non fatales, donc j'ai regardé quelques exemples de code (édité 2014-08-19 pour utiliser searchcode au lieu de Google Code Search, RIP). Sur les 10 résultats sélectionnés au hasard de la première page, tous contenaient des tests qui faisaient plusieurs assertions indépendantes dans la même méthode de test. Tous bénéficieraient d'assertions non fatales.

https://code.i-harness.com


Ce que vous voudrez probablement faire est de dériver unittest.TestCase car c'est la classe qui se déclenche quand une assertion échoue. Vous devrez réorganiser votre TestCase pour ne pas lancer (peut-être garder une liste d'échecs à la place). Ré-architecturer des choses peut causer d'autres problèmes que vous auriez à résoudre. Par exemple, vous devrez peut-être dériver TestSuite pour apporter des modifications à l'appui des modifications apportées à votre TestCase .


Est-ce que chacun affirme dans une méthode séparée.

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)

Il existe un paquet d'assertions soft dans PyPI appelé softest qui répondra à vos besoins. Il fonctionne en collectant les échecs, en combinant les données d'exception et de trace de pile, et en rapportant tout dans le cadre de la sortie unittest habituelle.

Par exemple, ce code:

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

... produit cette sortie de la console:

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

NOTE : J'ai créé et maintenez le softest .


J'ai aimé l'approche par @ Anthony-Batchelor, pour capturer l'exception AssertionError. Mais une légère variation de cette approche à l'aide de décorateurs et aussi un moyen de rapporter les cas de tests avec succès / échec.

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

Sortie de la console:

(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

Une autre façon d'avoir des assertions non fatales est de capturer l'exception d'assertion et de stocker les exceptions dans une liste. Affirmez ensuite que cette liste est vide dans le cadre de 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()

Une option est affirmer sur toutes les valeurs à la fois comme un tuple.

Par exemple:

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

La sortie de ces tests serait:

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

Cela montre que le modèle et le nombre de roues sont incorrects.





unit-testing