Poursuivre dans l'état unittest de Python lorsqu'une assertion échoue
EDIT: changé d’exemple et expliqué pourquoi il s’agit d’un problème réel.
J'aimerais écrire des tests unitaires en Python qui continuent à s'exécuter lorsqu'une assertion échoue, afin 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 le __init__
de la voiture définit correctement ses champs. Je pourrais le diviser 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 seule méthode qui teste un seul concept ("l'objet est correctement initialisé").
Si nous supposons qu'il est préférable ici de ne pas interrompre la méthode, j'ai un nouveau problème: je ne peux pas voir toutes les erreurs en même temps. Lorsque je corrige l'erreur model
et relance le test, l'erreur wheel_count
apparaît. Cela me ferait gagner du temps pour voir les deux erreurs lors de la première exécution du test.
À des fins de comparaison, le framework de tests unitaires C++ de Google fait la distinction entre entre les assertions EXPECT_*
non fatales et les assertions fatales ASSERT_*
:
Les assertions sont des paires qui testent la même chose mais ont des effets différents sur la fonction actuelle. Les versions d'ASSERT_ * génèrent des échecs fatals lorsqu'elles échouent et annulent la fonction en cours. Les versions EXPECT_ * génèrent des échecs non fatals, qui n'abandonnent pas la fonction en cours. En règle générale, EXPECT_ * est préférable, car il permet de signaler plusieurs défaillances dans un test. Cependant, vous devez utiliser ASSERT_ * s'il n'est pas logique de continuer lorsque l'assertion en question échoue.
Existe-t-il un moyen d'obtenir un comportement semblable à EXPECT_*
- dans la variable unittest
de Python? Si ce n'est pas le cas dans unittest
, existe-t-il un autre framework de test unitaire Python qui prend en charge ce comportement?
Incidemment, j'étais curieux de savoir combien de tests réels pourraient tirer profit d'affirmations non fatales. J'ai donc consulté quelques exemples de code (modifié le 2014-08-19 pour utiliser le code de recherche au lieu de Google Code Search, RIP). Sur 10 résultats choisis au hasard dans la première page, tous contenaient des tests ayant fait plusieurs assertions indépendantes dans la même méthode de test. Tous bénéficieraient d’affirmations non fatales.
Ce que vous voudrez probablement faire, c'est dériver unittest.TestCase
puisque c'est la classe qui se lève quand une assertion échoue. Vous devrez réarchiver votre TestCase
pour ne pas lancer (peut-être conserver une liste des échecs à la place). La réarchitecture peut créer d’autres problèmes que vous devrez résoudre. Par exemple, vous devrez peut-être dériver TestSuite
pour apporter des modifications à l'appui des modifications apportées à votre TestCase
.
Une autre façon d'avoir des assertions non fatales consiste à capturer l'exception d'assertion et à stocker les exceptions dans une liste. Ensuite, affirmez 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 __== "__main__":
unittest.main()
Une option est d'affirmer sur toutes les valeurs à la fois en tant que 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))
Le résultat 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.
Il est considéré comme un anti-modèle d'avoir plusieurs assertions dans un seul test unitaire. Un seul test unitaire ne devrait porter que sur une chose. Peut-être que vous testez trop. Envisagez de scinder ce test en plusieurs tests. De cette façon, vous pouvez nommer chaque test correctement.
Parfois, cependant, il est correct de vérifier plusieurs choses en même temps. Par exemple, lorsque vous affirmez les propriétés du même objet. Dans ce cas, vous affirmez en fait si cet objet est correct. Pour ce faire, écrivez une méthode d'assistance personnalisée qui sait comment affirmer cet objet. Vous pouvez écrire cette méthode de manière à afficher toutes les propriétés défaillantes ou, par exemple, l’état complet de l’objet attendu et l’état complet de l’objet réel lorsqu’une assertion échoue.
Faites chaque affirmation 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)
J'ai bien aimé l'approche de @ Anthony-Batchelor, qui consiste à capturer l'exception AssertionError. Mais une légère variation à cette approche en utilisant des décorateurs et aussi un moyen de signaler les cas de test 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 __== '__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
Il existe dans PyPI un package d’assertions souple appelé softest
qui répond à vos besoins. Cela fonctionne en collectant les échecs, en combinant les données de trace d'exception et de pile, et en les rapportant 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 __== '__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 mis à jour softest
.
expect est très utile dans gtest . Ceci est une manière python dans Gist , et code:
import sys
import unittest
class TestCase(unittest.TestCase):
def run(self, result=None):
if result is None:
self.result = self.defaultTestResult()
else:
self.result = result
return unittest.TestCase.run(self, result)
def expect(self, val, msg=None):
'''
Like TestCase.assert_, but doesn't halt the test.
'''
try:
self.assert_(val, msg)
except:
self.result.addFailure(self, sys.exc_info())
def expectEqual(self, first, second, msg=None):
try:
self.failUnlessEqual(first, second, msg)
except:
self.result.addFailure(self, sys.exc_info())
expect_equal = expectEqual
assert_equal = unittest.TestCase.assertEqual
assert_raises = unittest.TestCase.assertRaises
test_main = unittest.main
J'ai un problème avec @Anthony Batchelor answer, car cela me force à utiliser try...catch
dans mes tests unitaires. Ensuite, j'ai encapsulé la logique try...catch
dans un remplacement de la méthode TestCase.assertEqual
. Le hack suivant supprime les blocs try...catch
du code de tests unitaires:
import unittest
import traceback
class AssertionErrorData(object):
def __init__(self, stacktrace, message):
super(AssertionErrorData, self).__init__()
self.stacktrace = stacktrace
self.message = message
class MultipleAssertionFailures(unittest.TestCase):
def __init__(self, *args, **kwargs):
self.verificationErrors = []
super(MultipleAssertionFailures, self).__init__( *args, **kwargs )
def tearDown(self):
super(MultipleAssertionFailures, self).tearDown()
if self.verificationErrors:
index = 0
errors = []
for error in self.verificationErrors:
index += 1
errors.append( "%s\nAssertionError %s: %s" % (
error.stacktrace, index, error.message ) )
self.fail( '\n\n' + "\n".join( errors ) )
self.verificationErrors.clear()
def assertEqual(self, goal, results, msg=None):
try:
super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg )
except unittest.TestCase.failureException as error:
goodtraces = self._goodStackTraces()
self.verificationErrors.append(
AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) )
def _goodStackTraces(self):
"""
Get only the relevant part of stacktrace.
"""
stop = False
found = False
goodtraces = []
# stacktrace = traceback.format_exc()
# stacktrace = traceback.format_stack()
stacktrace = traceback.extract_stack()
# https://stackoverflow.com/questions/54499367/how-to-correctly-override-testcase
for stack in stacktrace:
filename = stack.filename
if found and not stop and \
not filename.find( 'lib' ) < filename.find( 'unittest' ):
stop = True
if not found and filename.find( 'lib' ) < filename.find( 'unittest' ):
found = True
if stop and found:
stackline = ' File "%s", line %s, in %s\n %s' % (
stack.filename, stack.lineno, stack.name, stack.line )
goodtraces.append( stackline )
return goodtraces
# class DummyTestCase(unittest.TestCase):
class DummyTestCase(MultipleAssertionFailures):
def setUp(self):
self.maxDiff = None
super(DummyTestCase, self).setUp()
def tearDown(self):
super(DummyTestCase, self).tearDown()
def test_function_name(self):
self.assertEqual( "var", "bar" )
self.assertEqual( "1937", "511" )
if __== '__main__':
unittest.main()
Résultat obtenu:
F
======================================================================
FAIL: test_function_name (__main__.DummyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\User\Downloads\test.py", line 77, in tearDown
super(DummyTestCase, self).tearDown()
File "D:\User\Downloads\test.py", line 29, in tearDown
self.fail( '\n\n' + "\n\n".join( errors ) )
AssertionError:
File "D:\User\Downloads\test.py", line 80, in test_function_name
self.assertEqual( "var", "bar" )
AssertionError 1: 'var' != 'bar'
- var
? ^
+ bar
? ^
:
File "D:\User\Downloads\test.py", line 81, in test_function_name
self.assertEqual( "1937", "511" )
AssertionError 2: '1937' != '511'
- 1937
+ 511
:
Des solutions alternatives pour la capture correcte de stacktrace pourraient être publiées sur Comment redéfinir correctement TestCase.assertEqual () pour produire le stacktrace correct?
Je sais que cette question a été posée littéralement il y a plusieurs années, mais il existe maintenant (au moins) deux packages Python permettant de le faire.
L'un est le plus doux: https://pypi.org/project/softest/
L'autre est Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Je n'ai pas utilisé non plus, mais ils me ressemblent beaucoup.
Je ne pense pas qu'il y ait un moyen de faire cela avec PyUnit et je ne voudrais pas que PyUnit soit étendu de cette manière.
Je préfère m'en tenir à une assertion par fonction de test ( ou plus spécifiquement, en affirmant un concept par test ) et réécrire test_addition()
sous la forme de quatre fonctions de test distinctes. Cela donnerait plus d'informations utiles en cas d'échec, viz:
.FF.
======================================================================
FAIL: test_addition_with_two_negatives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_addition.py", line 10, in test_addition_with_two_negatives
self.assertEqual(-1 + (-1), -1)
AssertionError: -2 != -1
======================================================================
FAIL: test_addition_with_two_positives (__main__.MathTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_addition.py", line 6, in test_addition_with_two_positives
self.assertEqual(1 + 1, 3) # Failure!
AssertionError: 2 != 3
----------------------------------------------------------------------
Ran 4 tests in 0.000s
FAILED (failures=2)
Si vous décidez que cette approche ne vous convient pas, vous pouvez trouver cette réponse utile.
Mettre à jour
Il semble que vous testiez deux concepts avec votre question mise à jour et que je les scinde en deux tests unitaires. La première est que les paramètres sont stockés lors de la création d'un nouvel objet. Cela aurait deux assertions, une pour make
et une pour model
. Si le premier échoue, il faut clairement régler le problème, que le second réussisse ou échoue soit sans importance à ce stade.
Le second concept est plus discutable ... Vous testez si certaines valeurs par défaut sont initialisées. Pourquoi? Il serait plus utile de tester ces valeurs au moment où elles sont réellement utilisées (et si elles ne sont pas utilisées, alors pourquoi sont-elles là?).
Ces deux tests échouent et les deux devraient l'être. Lorsque je fais des tests unitaires, je suis beaucoup plus intéressé par l'échec que par le succès, car c'est là que je dois me concentrer.
FF
======================================================================
FAIL: test_creation_defaults (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_car.py", line 25, in test_creation_defaults
self.assertEqual(self.car.wheel_count, 4) # Failure!
AssertionError: 3 != 4
======================================================================
FAIL: test_creation_parameters (__main__.CarTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_car.py", line 20, in test_creation_parameters
self.assertEqual(self.car.model, self.model) # Failure!
AssertionError: 'Ford' != 'Model T'
----------------------------------------------------------------------
Ran 2 tests in 0.000s
FAILED (failures=2)