Si j'écris des tests unitaires dans python (en utilisant le module unittest), est-il possible de générer des données à partir d'un test ayant échoué, afin que je puisse les examiner pour aider à déduire la cause de l'erreur? I Je suis conscient de la possibilité de créer un message personnalisé, qui peut contenir certaines informations, mais vous pouvez parfois traiter des données plus complexes qui ne peuvent pas être facilement représentées sous forme de chaîne.
Par exemple, supposons que vous ayez une classe Foo et que vous testiez une barre de méthode en utilisant les données d'une liste appelée testdata:
class TestBar(unittest.TestCase):
def runTest(self):
for t1, t2 in testdata:
f = Foo(t1)
self.assertEqual(f.bar(t2), 2)
Si le test échoue, je pourrais vouloir sortir t1, t2 et/ou f pour voir pourquoi ces données particulières ont entraîné un échec. En sortie, je veux dire que les variables sont accessibles comme n'importe quelle autre variable, après l'exécution du test.
Réponse très tardive pour quelqu'un qui, comme moi, vient ici à la recherche d'une réponse simple et rapide.
Dans Python 2.7, vous pouvez utiliser un paramètre supplémentaire msg
pour ajouter des informations au message d'erreur, comme suit:
self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))
Documents officiels ici
Nous utilisons le module de journalisation pour cela.
Par exemple:
import logging
class SomeTest( unittest.TestCase ):
def testSomething( self ):
log= logging.getLogger( "SomeTest.testSomething" )
log.debug( "this= %r", self.this )
log.debug( "that= %r", self.that )
# etc.
self.assertEquals( 3.14, pi )
if __== "__main__":
logging.basicConfig( stream=sys.stderr )
logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
unittest.main()
Cela nous permet d'activer le débogage pour des tests spécifiques dont nous savons qu'ils échouent et pour lesquels nous voulons des informations de débogage supplémentaires.
Ma méthode préférée, cependant, ne consiste pas à passer beaucoup de temps à déboguer, mais à passer des tests plus détaillés à exposer le problème.
Vous pouvez utiliser des instructions d'impression simples, ou tout autre moyen d'écrire sur stdout. Vous pouvez également invoquer le débogueur Python) n’importe où dans vos tests.
Si vous utilisez nose pour exécuter vos tests (ce que je recommande), il collectera la sortie standard pour chaque test et ne vous le montrera que si le test échoue. Vous n'avez donc pas à vivre avec le sortie encombrée lorsque les tests sont réussis.
nose a également des commutateurs pour afficher automatiquement les variables mentionnées dans les assertions, ou pour appeler le débogueur en cas d'échec des tests. Par exemple -s
(--nocapture
) empêche la capture de stdout.
Je ne pense pas que vous recherchiez ce que vous cherchez. Il n’ya aucun moyen d’afficher des valeurs variables qui n’échouent pas, mais cela peut vous aider à vous rapprocher de l’obtention des résultats comme vous le souhaitez.
Vous pouvez utiliser le objet TestResult renvoyé par le TestRunner.run () pour l'analyse et le traitement des résultats. En particulier, TestResult.errors et TestResult.failures
A propos de l'objet TestResults:
http://docs.python.org/library/unittest.html#id
Et du code pour vous orienter dans la bonne direction:
>>> import random
>>> import unittest
>>>
>>> class TestSequenceFunctions(unittest.TestCase):
... def setUp(self):
... self.seq = range(5)
... def testshuffle(self):
... # make sure the shuffled sequence does not lose any elements
... random.shuffle(self.seq)
... self.seq.sort()
... self.assertEqual(self.seq, range(10))
... def testchoice(self):
... element = random.choice(self.seq)
... error_test = 1/0
... self.assert_(element in self.seq)
... def testsample(self):
... self.assertRaises(ValueError, random.sample, self.seq, 20)
... for element in random.sample(self.seq, 5):
... self.assert_(element in self.seq)
...
>>> suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
>>> testResult = unittest.TextTestRunner(verbosity=2).run(suite)
testchoice (__main__.TestSequenceFunctions) ... ERROR
testsample (__main__.TestSequenceFunctions) ... ok
testshuffle (__main__.TestSequenceFunctions) ... FAIL
======================================================================
ERROR: testchoice (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<stdin>", line 11, in testchoice
ZeroDivisionError: integer division or modulo by zero
======================================================================
FAIL: testshuffle (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
File "<stdin>", line 8, in testshuffle
AssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
----------------------------------------------------------------------
Ran 3 tests in 0.031s
FAILED (failures=1, errors=1)
>>>
>>> testResult.errors
[(<__main__.TestSequenceFunctions testMethod=testchoice>, 'Traceback (most recent call last):\n File "<stdin>"
, line 11, in testchoice\nZeroDivisionError: integer division or modulo by zero\n')]
>>>
>>> testResult.failures
[(<__main__.TestSequenceFunctions testMethod=testshuffle>, 'Traceback (most recent call last):\n File "<stdin>
", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')]
>>>
Je pense que j'ai peut-être trop réfléchi à cela. L’une des méthodes que j’ai trouvées pour faire le travail est simplement d’avoir une variable globale qui accumule les données de diagnostic.
Quelque chose comme ça:
log1 = dict()
class TestBar(unittest.TestCase):
def runTest(self):
for t1, t2 in testdata:
f = Foo(t1)
if f.bar(t2) != 2:
log1("TestBar.runTest") = (f, t1, t2)
self.fail("f.bar(t2) != 2")
Merci pour les resplies. Ils m'ont donné quelques idées alternatives sur la manière d'enregistrer des informations à partir de tests unitaires.
Une autre option - démarrer un débogueur où le test échoue.
Essayez d'exécuter vos tests avec Testoob (votre suite unittest sera exécutée sans modifications) et vous pouvez utiliser le commutateur de ligne de commande '--debug' pour ouvrir un débogueur en cas d'échec d'un test.
Voici une session de terminal sur Windows:
C:\work> testoob tests.py --debug
F
Debugging for failure in test: test_foo (tests.MyTests.test_foo)
> c:\python25\lib\unittest.py(334)failUnlessEqual()
-> (msg or '%r != %r' % (first, second))
(Pdb) up
> c:\work\tests.py(6)test_foo()
-> self.assertEqual(x, y)
(Pdb) l
1 from unittest import TestCase
2 class MyTests(TestCase):
3 def test_foo(self):
4 x = 1
5 y = 2
6 -> self.assertEqual(x, y)
[EOF]
(Pdb)
La méthode que j'utilise est vraiment simple. Je me connecte juste comme un avertissement afin qu'il apparaisse réellement.
import logging
class TestBar(unittest.TestCase):
def runTest(self):
#this line is important
logging.basicConfig()
log = logging.getLogger("LOG")
for t1, t2 in testdata:
f = Foo(t1)
self.assertEqual(f.bar(t2), 2)
log.warning(t1)
Vous pouvez utiliser le module logging
pour cela.
Donc, dans le code de test unitaire, utilisez:
import logging as log
def test_foo(self):
log.debug("Some debug message.")
log.info("Some info message.")
log.warning("Some warning message.")
log.error("Some error message.")
Par défaut, les avertissements et les erreurs sont générés dans /dev/stderr
, elles doivent donc être visibles sur la console.
Pour personnaliser les journaux (tels que le formatage), essayez l'exemple suivant:
# Set-up logger
if args.verbose or args.debug:
logging.basicConfig( stream=sys.stdout )
root = logging.getLogger()
root.setLevel(logging.INFO if args.verbose else logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO if args.verbose else logging.DEBUG)
ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
root.addHandler(ch)
else:
logging.basicConfig(stream=sys.stderr)
Utiliser la journalisation:
import unittest
import logging
import inspect
import os
logging_level = logging.INFO
try:
log_file = os.environ["LOG_FILE"]
except KeyError:
log_file = None
def logger(stack=None):
if not hasattr(logger, "initialized"):
logging.basicConfig(filename=log_file, level=logging_level)
logger.initialized = True
if not stack:
stack = inspect.stack()
name = stack[1][3]
try:
name = stack[1][0].f_locals["self"].__class__.__+ "." + name
except KeyError:
pass
return logging.getLogger(name)
def todo(msg):
logger(inspect.stack()).warning("TODO: {}".format(msg))
def get_pi():
logger().info("sorry, I know only three digits")
return 3.14
class Test(unittest.TestCase):
def testName(self):
todo("use a better get_pi")
pi = get_pi()
logger().info("pi = {}".format(pi))
todo("check more digits in pi")
self.assertAlmostEqual(pi, 3.14)
logger().debug("end of this test")
pass
Usage:
# LOG_FILE=/tmp/log python3 -m unittest LoggerDemo
.
----------------------------------------------------------------------
Ran 1 test in 0.047s
OK
# cat /tmp/log
WARNING:Test.testName:TODO: use a better get_pi
INFO:get_pi:sorry, I know only three digits
INFO:Test.testName:pi = 3.14
WARNING:Test.testName:TODO: check more digits in pi
Si vous ne définissez pas LOG_FILE
, la journalisation devra aboutir à stderr
.
Ce que je fais dans ces cas-là, c'est d'avoir un log.debug()
avec quelques messages dans mon application. Étant donné que le niveau de consignation par défaut est WARNING
, ces messages ne s'affichent pas lors de l'exécution normale.
Ensuite, dans les modifications unittest, je modifie le niveau de journalisation en DEBUG
, afin que ces messages soient affichés lors de leur exécution.
import logging
log.debug("Some messages to be shown just when debugging or unittesting")
Dans les unittests:
# Set log level
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)
Voir un exemple complet:
Ceci est daikiri.py
, Une classe de base qui implémente un Daikiri avec son nom et son prix. Il existe une méthode make_discount()
qui renvoie le prix de ce daikiri spécifique après application d'un rabais donné:
import logging
log = logging.getLogger(__name__)
class Daikiri(object):
def __init__(self, name, price):
self.name = name
self.price = price
def make_discount(self, percentage):
log.debug("Deducting discount...") # I want to see this message
return self.price * percentage
Ensuite, je crée un unestest test_daikiri.py
Qui vérifie son utilisation:
import unittest
import logging
from .daikiri import Daikiri
class TestDaikiri(unittest.TestCase):
def setUp(self):
# Changing log level to DEBUG
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)
self.mydaikiri = Daikiri("cuban", 25)
def test_drop_price(self):
new_price = self.mydaikiri.make_discount(0)
self.assertEqual(new_price, 0)
if __== "__main__":
unittest.main()
Ainsi, lorsque je l'exécute, je reçois les messages log.debug
:
$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
inspect.trace vous permettra d’obtenir les variables locales après la levée d’une exception. Vous pouvez ensuite envelopper les tests unitaires avec un décorateur comme le suivant pour enregistrer ces variables locales en vue de leur examen au cours de l'autopsie.
import random
import unittest
import inspect
def store_result(f):
"""
Store the results of a test
On success, store the return value.
On failure, store the local variables where the exception was thrown.
"""
def wrapped(self):
if 'results' not in self.__dict__:
self.results = {}
# If a test throws an exception, store local variables in results:
try:
result = f(self)
except Exception as e:
self.results[f.__name__] = {'success':False, 'locals':inspect.trace()[-1][0].f_locals}
raise e
self.results[f.__name__] = {'success':True, 'result':result}
return result
return wrapped
def suite_results(suite):
"""
Get all the results from a test suite
"""
ans = {}
for test in suite:
if 'results' in test.__dict__:
ans.update(test.results)
return ans
# Example:
class TestSequenceFunctions(unittest.TestCase):
def setUp(self):
self.seq = range(10)
@store_result
def test_shuffle(self):
# make sure the shuffled sequence does not lose any elements
random.shuffle(self.seq)
self.seq.sort()
self.assertEqual(self.seq, range(10))
# should raise an exception for an immutable sequence
self.assertRaises(TypeError, random.shuffle, (1,2,3))
return {1:2}
@store_result
def test_choice(self):
element = random.choice(self.seq)
self.assertTrue(element in self.seq)
return {7:2}
@store_result
def test_sample(self):
x = 799
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
return {1:99999}
suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
unittest.TextTestRunner(verbosity=2).run(suite)
from pprint import pprint
pprint(suite_results(suite))
La dernière ligne affichera les valeurs renvoyées où le test a réussi et les variables locales, dans ce cas x, en cas d'échec:
{'test_choice': {'result': {7: 2}, 'success': True},
'test_sample': {'locals': {'self': <__main__.TestSequenceFunctions testMethod=test_sample>,
'x': 799},
'success': False},
'test_shuffle': {'result': {1: 2}, 'success': True}}
Har det gøy :-)
Que diriez-vous d'attraper l'exception qui est générée à partir de l'échec de l'assertion? Dans votre bloc catch, vous pouvez exporter les données comme vous le souhaitez, où que vous soyez. Ensuite, lorsque vous aurez terminé, vous pourrez relancer l’exception. Le coureur de test ne connaîtrait probablement pas la différence.
Avertissement: je n'ai pas essayé cela avec le framework de test unitaire de python mais avec d'autres frameworks de test unitaire.
En admettant que je ne l'ai pas essayé, le fonction de journalisation de testfixtures est plutôt utile ...