Sortie des données du test unitaire en python

115

Si j'écris des tests unitaires en python (en utilisant le module unittest), est-il possible de sortir des données d'un test ayant échoué, afin que je puisse l'examiner pour aider à déduire la cause de l'erreur? Je suis conscient de la possibilité de créer un message personnalisé, qui peut contenir des informations, mais parfois vous pouvez 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éthodes, 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 a échoué, je pourrais vouloir sortir t1, t2 et / ou f, pour voir pourquoi ces données particulières ont entraîné un échec. Par sortie, je veux dire que les variables sont accessibles comme toutes les autres variables, une fois le test exécuté.

Poisson d'argent
la source

Réponses:

73

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 msgpour ajouter des informations au message d'erreur comme ceci:

self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))

Documents officiels ici

Facundo Casco
la source
1
Fonctionne également en Python 3.
MrDBA
18
La documentation fait allusion à cela, mais cela vaut la peine de le mentionner explicitement: par défaut, s'il msgest utilisé, il remplacera le message d'erreur normal. Pour être msgajouté au message d'erreur normal, vous devez également définir TestCase.longMessage sur True
Catalin Iacob
1
bon à savoir, nous pouvons transmettre un message d'erreur personnalisé, mais j'étais intéressé par l'impression d'un message indépendamment de l'erreur.
Harry Moreno
5
Le commentaire de @CatalinIacob s'applique à Python 2.x. Dans Python 3.x, TestCase.longMessage prend la valeur par défaut True.
ndmeiri
70

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 __name__ == "__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, n'est pas de passer beaucoup de temps sur le débogage, mais de le passer à écrire des tests plus fins pour exposer le problème.

S.Lott
la source
Que faire si j'appelle une méthode foo à l'intérieur de testSomething et qu'elle enregistre quelque chose. Comment puis-je voir la sortie pour cela sans passer l'enregistreur à foo?
simao
@simao: Qu'est-ce que c'est foo? Une fonction distincte? Une méthode fonction de SomeTest? Dans le premier cas, une fonction peut avoir son propre enregistreur. Dans le second cas, l'autre fonction de méthode peut avoir son propre enregistreur. Connaissez-vous le fonctionnement du loggingpackage? Plusieurs enregistreurs sont la norme.
S.Lott
8
J'ai configuré la journalisation de la manière exacte que vous avez spécifiée. Je suppose que cela fonctionne, mais où puis-je voir la sortie? Il ne sort pas sur la console. J'ai essayé de le configurer avec la journalisation dans un fichier, mais cela ne produit pas non plus de sortie.
MikeyE
"Ma méthode préférée, cependant, n'est pas de passer beaucoup de temps sur le débogage, mais de le passer à écrire des tests plus fins pour exposer le problème." -- bien dit!
Seth
34

Vous pouvez utiliser des instructions d'impression simples ou tout autre moyen d'écrire dans stdout. Vous pouvez également appeler 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 le stdout pour chaque test et ne vous le montrera que si le test a échoué, vous n'avez donc pas à vivre avec la sortie encombrée lorsque les tests réussissent.

nose a également des commutateurs pour afficher automatiquement les variables mentionnées dans les assertions ou pour invoquer le débogueur en cas d'échec des tests. Par exemple -s( --nocapture) empêche la capture de stdout.

Ned Batchelder
la source
Malheureusement, nose ne semble pas collecter le journal écrit sur stdout / err en utilisant le framework de journalisation. J'ai le printet log.debug()l'un à côté de l'autre, et j'active explicitement la DEBUGjournalisation à la racine de la setUp()méthode, mais seule la printsortie apparaît.
haridsv
7
nosetests -smontre le contenu de stdout s'il y a une erreur ou non - quelque chose que je trouve utile.
hargriffle
Je ne trouve pas les commutateurs pour afficher automatiquement les variables dans la documentation sur le nez. Pouvez-vous m'indiquer quelque chose qui les décrit?
ABM
Je ne connais pas de moyen d'afficher automatiquement les variables de nose ou unittest. J'imprime les choses que je veux voir dans mes tests.
Ned Batchelder
16

Je ne pense pas que ce soit tout à fait ce que vous recherchez, il n'y a aucun moyen d'afficher des valeurs de variable qui n'échouent pas, mais cela peut vous aider à vous rapprocher de la sortie des résultats comme vous le souhaitez.

Vous pouvez utiliser l' objet TestResult renvoyé par TestRunner.run () pour l'analyse et le traitement des résultats. En particulier, TestResult.errors et TestResult.failures

À propos de l'objet TestResults:

http://docs.python.org/library/unittest.html#id3

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')]
>>>
Monkut
la source
5

Une autre option - démarrer un débogueur là où le test échoue.

Essayez d'exécuter vos tests avec Testoob (il exécutera votre suite unittest sans modifications), et vous pouvez utiliser le commutateur de ligne de commande '--debug' pour ouvrir un débogueur lorsqu'un test échoue.

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)
orip
la source
2
Nose ( nose.readthedocs.org/en/latest/index.html ) est un autre framework qui fournit des options «démarrer une session de débogage». Je l'exécute avec '-sx --pdb --pdb-failures', qui ne mange pas la sortie, s'arrête après le premier échec et tombe dans pdb en cas d'exceptions et d'échecs de test. Cela a supprimé mon besoin de messages d'erreur riches, à moins que je ne sois paresseux et que je teste en boucle.
jwhitlock
5

La méthode que j'utilise est vraiment simple. Je l'enregistre simplement comme un avertissement pour 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)
Orane
la source
Cela fonctionnera-t-il si le test réussit? Dans mon cas, l'avertissement ne s'affiche que si le test échoue
Shreya Maria
@ShreyaMaria yes it will
Orane
5

Je pense que j'y ai peut-être trop réfléchi. Une façon dont je suis venu avec qui fait 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 réponses. Ils m'ont donné des idées alternatives sur la façon d'enregistrer des informations à partir de tests unitaires en python.

Poisson d'argent
la source
2

Utilisez 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__ + "." + 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 sera effectuée stderr.

pas un utilisateur
la source
2

Vous pouvez utiliser le loggingmodule 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 envoyés vers /dev/stderr, ils doivent donc être visibles sur la console.

Pour personnaliser les journaux (tels que la mise en forme), 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)
Kenorb
la source
2

Ce que je fais dans ces cas est d'avoir un log.debug()avec quelques messages dans mon application. Étant donné que le niveau de journalisation par défaut est WARNING, ces messages ne s'affichent pas dans l'exécution normale.

Ensuite, dans le test unittest, je change 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:

Il s'agit d' daikiri.pyune 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 avoir appliqué une remise donnée:

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 unittest test_daikiri.pyqui 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 __name__ == "__main__":
    unittest.main()

Donc, quand je l'exécute, je reçois les log.debugmessages:

$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
fedorqui 'Alors arrêtez de nuire'
la source
1

inspect.trace vous permettra d'obtenir des variables locales après qu'une exception a été levée. Vous pouvez ensuite envelopper les tests unitaires avec un décorateur comme le suivant pour enregistrer ces variables locales pour examen lors 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 là 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 :-)

Max Murphy
la source
0

Que diriez-vous d'attraper l'exception qui est générée à partir de l'échec d'assertion? Dans votre bloc catch, vous pouvez sortir les données comme vous le souhaitez, où que vous soyez. Ensuite, lorsque vous avez terminé, vous pouvez relancer l'exception. Le testeur 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.

Sam Corder
la source
-1

En élargissant la réponse de @FC, cela fonctionne très bien pour moi:

class MyTest(unittest.TestCase):
    def messenger(self, message):
        try:
            self.assertEqual(1, 2, msg=message)
        except AssertionError as e:      
            print "\nMESSENGER OUTPUT: %s" % str(e),
Georgepsarakis
la source