Ausgabe von Daten aus dem Unit-Test in Python

115

Wenn ich Unit-Tests in Python schreibe (mit dem unittest-Modul), ist es dann möglich, Daten aus einem fehlgeschlagenen Test auszugeben, damit ich sie untersuchen kann, um herauszufinden, was den Fehler verursacht hat? Ich bin mir der Möglichkeit bewusst, eine benutzerdefinierte Nachricht zu erstellen, die einige Informationen enthalten kann, aber manchmal können Sie mit komplexeren Daten umgehen, die nicht einfach als Zeichenfolge dargestellt werden können.

Angenommen, Sie hatten eine Klasse Foo und testeten eine Methodenleiste mit Daten aus einer Liste namens testdata:

class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1)
            self.assertEqual(f.bar(t2), 2)

Wenn der Test fehlgeschlagen ist, möchte ich möglicherweise t1, t2 und / oder f ausgeben, um festzustellen, warum diese bestimmten Daten zu einem Fehler geführt haben. Mit Ausgabe meine ich, dass auf die Variablen wie auf alle anderen Variablen zugegriffen werden kann, nachdem der Test ausgeführt wurde.

Silberfisch
quelle

Antworten:

73

Sehr späte Antwort für jemanden, der wie ich hierher kommt und nach einer einfachen und schnellen Antwort sucht.

In Python 2.7 können Sie einen zusätzlichen Parameter verwenden msg, um der Fehlermeldung folgende Informationen hinzuzufügen:

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

Offizielle Dokumente hier

Facundo Casco
quelle
1
Funktioniert auch in Python 3.
MrDBA
18
Die Dokumentation weist darauf hin, msgist jedoch ausdrücklich zu erwähnen: Wenn diese Option verwendet wird, wird standardmäßig die normale Fehlermeldung ersetzt. Um msgan die normale Fehlermeldung angehängt zu werden, müssen Sie auch TestCase.longMessage auf True setzen
Catalin Iacob
1
Gut zu wissen, dass wir eine benutzerdefinierte Fehlermeldung übergeben können, aber ich war daran interessiert, eine Nachricht unabhängig vom Fehler zu drucken.
Harry Moreno
5
Der Kommentar von @CatalinIacob gilt für Python 2.x. In Python 3.x ist TestCase.longMessage standardmäßig True.
Ndmeiri
70

Wir verwenden dafür das Protokollierungsmodul.

Beispielsweise:

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

Auf diese Weise können wir das Debuggen für bestimmte Tests aktivieren, von denen wir wissen, dass sie fehlschlagen und für die wir zusätzliche Debugging-Informationen benötigen.

Meine bevorzugte Methode ist jedoch nicht, viel Zeit mit dem Debuggen zu verbringen, sondern feinere Tests zu schreiben, um das Problem aufzudecken.

S.Lott
quelle
Was ist, wenn ich eine Methode foo in testSomething aufrufe und sie etwas protokolliert? Wie kann ich die Ausgabe dafür sehen, ohne den Logger an foo zu übergeben?
Simao
@simao: Was ist foo? Eine separate Funktion? Eine Methodenfunktion von SomeTest? Im ersten Fall kann eine Funktion einen eigenen Logger haben. Im zweiten Fall kann die andere Methodenfunktion einen eigenen Logger haben. Wissen Sie, wie das loggingPaket funktioniert? Mehrere Logger sind die Norm.
S.Lott
8
Ich habe die Protokollierung genau so eingerichtet, wie Sie es angegeben haben. Ich nehme an, es funktioniert, aber wo sehe ich die Ausgabe? Es wird nicht an die Konsole ausgegeben. Ich habe versucht, es mit der Protokollierung in einer Datei zu konfigurieren, aber das erzeugt auch keine Ausgabe.
MikeyE
"Meine bevorzugte Methode besteht jedoch nicht darin, viel Zeit für das Debuggen aufzuwenden, sondern dafür, feinkörnigere Tests zu schreiben, um das Problem aufzudecken." -- gut gesagt!
Seth
34

Sie können einfache Druckanweisungen oder eine andere Schreibweise für stdout verwenden. Sie können den Python-Debugger auch an einer beliebigen Stelle in Ihren Tests aufrufen.

Wenn Sie Ihre Tests mit der Nase ausführen (was ich empfehle), wird die Standardausgabe für jeden Test erfasst und nur angezeigt, wenn der Test fehlgeschlagen ist, sodass Sie beim Bestehen der Tests nicht mit der überfüllten Ausgabe leben müssen.

Nase hat auch Schalter, um die in Asserts erwähnten Variablen automatisch anzuzeigen oder den Debugger bei fehlgeschlagenen Tests aufzurufen. Zum Beispiel -s( --nocapture) verhindert die Erfassung von stdout.

Ned Batchelder
quelle
Leider scheint die Nase kein Protokoll zu sammeln, das mit dem Protokollierungsframework in stdout / err geschrieben wurde. Ich habe das printund log.debug()nebeneinander und aktiviere explizit die DEBUGProtokollierung im Stammverzeichnis der setUp()Methode, aber nur die printAusgabe wird angezeigt.
Haridsv
7
nosetests -szeigt den Inhalt von stdout, ob ein Fehler vorliegt oder nicht - etwas, das ich nützlich finde.
Hargriffle
Ich kann die Schalter zum automatischen Anzeigen von Variablen in den Nasendokumenten nicht finden. Können Sie mich auf etwas hinweisen, das sie beschreibt?
ABM
Ich kenne keine Möglichkeit, Variablen automatisch von der Nase oder von unittest anzuzeigen. Ich drucke die Dinge aus, die ich in meinen Tests sehen möchte.
Ned Batchelder
16

Ich denke nicht, dass dies genau das ist, wonach Sie suchen. Es gibt keine Möglichkeit, Variablenwerte anzuzeigen, die nicht fehlschlagen. Dies kann Ihnen jedoch dabei helfen, die Ausgabe der gewünschten Ergebnisse näher zu bringen.

Sie können das von TestRunner.run () zurückgegebene TestResult-Objekt zur Ergebnisanalyse und -verarbeitung verwenden. Insbesondere TestResult.errors und TestResult.failures

Informationen zum TestResults-Objekt:

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

Und ein Code, der Sie in die richtige Richtung weist:

>>> 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')]
>>>
Mönch
quelle
5

Eine weitere Option: Starten Sie einen Debugger, bei dem der Test fehlschlägt.

Versuchen Sie, Ihre Tests mit Testoob auszuführen (es wird Ihre unittest Suite ohne Änderungen ausführen), und Sie können den Befehlszeilenschalter '--debug' verwenden, um einen Debugger zu öffnen, wenn ein Test fehlschlägt.

Hier ist eine Terminalsitzung unter 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
quelle
2
Nose ( Nase.readthedocs.org/en/latest/index.html ) ist ein weiteres Framework, das Optionen zum Starten einer Debugger-Sitzung bietet. Ich führe es mit '-sx --pdb --pdb-failed' aus, das keine Ausgabe frisst, nach dem ersten Fehler stoppt und bei Ausnahmen und Testfehlern in pdb abfällt. Dies hat meine Notwendigkeit für umfangreiche Fehlermeldungen beseitigt, es sei denn, ich bin faul und teste in einer Schleife.
Jwhitlock
5

Die Methode, die ich benutze, ist wirklich einfach. Ich protokolliere es nur als Warnung, damit es tatsächlich angezeigt wird.

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
quelle
Funktioniert dies, wenn der Test erfolgreich ist? In meinem Fall wird die Warnung nur
angezeigt,
@ShreyaMaria ja wird es
Orane
5

Ich glaube, ich hätte das überlegt. Eine Möglichkeit, die ich mir ausgedacht habe, besteht darin, einfach eine globale Variable zu haben, die die Diagnosedaten sammelt.

So etwas in der Art:

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

Danke für die Antworten. Sie haben mir einige alternative Ideen gegeben, wie man Informationen aus Unit-Tests in Python aufzeichnet.

Silberfisch
quelle
2

Protokollierung verwenden:

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

Verwendung:

# 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

Wenn Sie nicht festlegen LOG_FILE, wird die Protokollierung durchgeführt stderr.

kein Benutzer
quelle
2

Sie können das loggingModul dafür verwenden.

Verwenden Sie also im Unit-Test-Code:

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

Standardmäßig werden Warnungen und Fehler ausgegeben /dev/stderr, sodass sie auf der Konsole sichtbar sein sollten.

Versuchen Sie das folgende Beispiel, um Protokolle anzupassen (z. B. die Formatierung):

# 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
quelle
2

Was ich in diesen Fällen mache, ist eine log.debug()mit einigen Nachrichten in meiner Anwendung zu haben. Da die Standardprotokollierungsstufe ist WARNING, werden solche Nachrichten in der normalen Ausführung nicht angezeigt .

Dann ändere ich im Unittest die Protokollierungsstufe auf DEBUG, so dass solche Nachrichten angezeigt werden, während sie ausgeführt werden.

import logging

log.debug("Some messages to be shown just when debugging or unittesting")

In den Unittests:

# Set log level
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)



Ein vollständiges Beispiel anzeigen:

Dies ist daikiri.pyeine Grundklasse, die einen Daikiri mit seinem Namen und Preis implementiert. Es gibt eine Methode make_discount(), die den Preis dieses bestimmten Daikiri nach Anwendung eines bestimmten Rabatts zurückgibt:

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

Dann erstelle ich ein Unittest test_daikiri.py, das seine Verwendung überprüft:

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

Wenn ich es ausführe, erhalte ich folgende log.debugMeldungen:

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

OK
fedorqui 'SO hör auf zu schaden'
quelle
1

Mit inspect.trace können Sie lokale Variablen abrufen, nachdem eine Ausnahme ausgelöst wurde. Anschließend können Sie die Komponententests mit einem Dekorateur wie dem folgenden abschließen, um diese lokalen Variablen für die Untersuchung während der Obduktion zu speichern.

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

In der letzten Zeile werden die zurückgegebenen Werte gedruckt, bei denen der Test erfolgreich war, und die lokalen Variablen, in diesem Fall x, wenn dies fehlschlägt:

{'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
quelle
0

Wie wäre es, wenn Sie die Ausnahme abfangen, die durch den Assertionsfehler generiert wird? In Ihrem catch-Block können Sie die Daten nach Belieben ausgeben, wo immer Sie möchten. Wenn Sie fertig sind, können Sie die Ausnahme erneut auslösen. Der Testläufer würde den Unterschied wahrscheinlich nicht kennen.

Haftungsausschluss: Ich habe dies nicht mit dem Unit-Test-Framework von Python versucht, sondern mit anderen Unit-Test-Frameworks.

Sam Corder
quelle
-1

Wenn ich die Antwort von @FC erweitere, funktioniert das ganz gut für mich:

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
quelle