Output di dati da unit test in python


115

Se sto scrivendo unit test in python (usando il modulo unittest), è possibile produrre dati da un test fallito, quindi posso esaminarlo per aiutare a dedurre cosa ha causato l'errore? Sono consapevole della possibilità di creare un messaggio personalizzato, che può contenere alcune informazioni, ma a volte potresti trattare dati più complessi, che non possono essere facilmente rappresentati come una stringa.

Ad esempio, supponiamo di avere una classe Foo e di testare una barra dei metodi, utilizzando i dati di un elenco chiamato testdata:

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

Se il test non è riuscito, potrei voler visualizzare t1, t2 e / o f, per vedere perché questo particolare dato ha provocato un errore. Per output, intendo che è possibile accedere alle variabili come qualsiasi altra variabile, dopo che il test è stato eseguito.

Risposte:


73

Risposta tardiva per qualcuno che, come me, viene qui in cerca di una risposta semplice e veloce.

In Python 2.7 è possibile utilizzare un parametro aggiuntivo msgper aggiungere informazioni al messaggio di errore in questo modo:

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

Documenti ufficiali qui


1
Funziona anche in Python 3.
MrDBA

18
La documentazione suggerisce questo, ma vale la pena menzionarlo esplicitamente: per impostazione predefinita, se msgviene utilizzato, sostituirà il normale messaggio di errore. Per essere msgaggiunto al normale messaggio di errore, devi anche impostare TestCase.longMessage su True
Catalin Iacob

1
Buono a sapersi che possiamo passare un messaggio di errore personalizzato, ma ero interessato a stampare un messaggio indipendentemente dall'errore.
Harry Moreno,

5
Il commento di @CatalinIacob si applica a Python 2.x. In Python 3.x, il valore predefinito di TestCase.longMessage èTrue .
ndmeiri

70

Per questo usiamo il modulo di registrazione.

Per esempio:

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

Ciò ci consente di attivare il debug per test specifici che sappiamo non riescono e per i quali desideriamo ulteriori informazioni di debug.

Il mio metodo preferito, tuttavia, non è quello di dedicare molto tempo al debug, ma dedicarlo alla scrittura di test più dettagliati per esporre il problema.


Cosa succede se chiamo un metodo pippo all'interno di testSomething e registra qualcosa. Come posso vedere l'output per quello senza passare il logger a foo?
simao

@simao: che cos'è foo? Una funzione separata? Una funzione di metodo di SomeTest? Nel primo caso, una funzione può avere il proprio logger. Nel secondo caso, l'altra funzione del metodo può avere il proprio logger. Sei a conoscenza di come funziona il loggingpacchetto? Più logger sono la norma.
S.Lott

8
Ho impostato la registrazione nel modo esatto da te specificato. Presumo stia funzionando, ma dove vedo l'output? Non sta trasmettendo alla console. Ho provato a configurarlo con la registrazione in un file, ma neanche questo produce alcun output.
MikeyE

"Il mio metodo preferito, tuttavia, non è quello di dedicare molto tempo al debug, ma dedicarlo alla scrittura di test più dettagliati per esporre il problema." -- ben detto!
Seth

34

È possibile utilizzare semplici istruzioni di stampa o qualsiasi altro modo di scrivere su stdout. Puoi anche richiamare il debugger Python ovunque nei tuoi test.

Se usi il naso per eseguire i tuoi test (cosa che consiglio), raccoglierà lo stdout per ogni test e te lo mostrerà solo se il test fallisce, quindi non devi vivere con l'output disordinato quando i test passano.

nose ha anche opzioni per mostrare automaticamente le variabili menzionate in asserts, o per invocare il debugger in caso di test falliti. Ad esempio -s( --nocapture) impedisce la cattura di stdout.


Sfortunatamente, nose non sembra raccogliere log scritti su stdout / err usando il framework di registrazione. Ho il printe uno log.debug()accanto all'altro e accendo esplicitamente la DEBUGregistrazione alla radice dal setUp()metodo, ma viene visualizzato solo l' printoutput.
haridsv

7
nosetests -smostra il contenuto di stdout sia che ci sia un errore o meno - qualcosa che trovo utile.
hargriffle

Non riesco a trovare gli interruttori per mostrare automaticamente le variabili nei documenti del naso. Puoi indicarmi qualcosa che li descriva?
ABM

Non conosco un modo per mostrare automaticamente le variabili da nose o unittest. Stampa le cose che voglio vedere nei miei test.
Ned Batchelder

16

Non penso che questo sia esattamente quello che stai cercando, non c'è modo di visualizzare i valori delle variabili che non falliscono, ma questo potrebbe aiutarti ad avvicinarti all'output dei risultati nel modo desiderato.

È possibile utilizzare l' oggetto TestResult restituito da TestRunner.run () per l'analisi e l'elaborazione dei risultati. In particolare, TestResult.errors e TestResult.failures

Informazioni sull'oggetto TestResults:

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

E un po 'di codice per indirizzarti nella giusta direzione:

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

5

Un'altra opzione: avvia un debugger in cui il test fallisce.

Prova a eseguire i tuoi test con Testoob (eseguirà la tua suite unittest senza modifiche) e puoi utilizzare l'opzione della riga di comando '--debug' per aprire un debugger quando un test fallisce.

Ecco una sessione terminale su 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)

2
Nose ( nose.readthedocs.org/en/latest/index.html ) è un altro framework che fornisce le opzioni di "avvio di una sessione di debugger". Lo eseguo con '-sx --pdb --pdb-failures', che non mangia l'output, si ferma dopo il primo errore e cade in pdb in caso di eccezioni e test falliti. Ciò ha eliminato la mia necessità di messaggi di errore ricchi, a meno che non sia pigro e stia testando in un ciclo.
jwhitlock

5

Il metodo che utilizzo è davvero semplice. L'ho solo registrato come avviso, quindi verrà effettivamente visualizzato.

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)

Funzionerà se il test avrà esito positivo? Nel mio caso l'avviso viene visualizzato solo se il test fallisce
Shreya Maria

@ShreyaMaria sì lo farà
Orane,

5

Penso che avrei potuto pensarci troppo. Un modo in cui ho pensato che funzioni, è semplicemente di avere una variabile globale, che accumula i dati diagnostici.

Qualcosa del genere:

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

Grazie per le risposte. Mi hanno dato alcune idee alternative su come registrare le informazioni dai test unitari in Python.


2

Usa registrazione:

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

Uso:

# 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

Se non si imposta LOG_FILE, la registrazione andrà a stderr.


2

Puoi usare logging modulo per questo.

Quindi nel codice dello unit test, usa:

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

Per impostazione predefinita, gli avvisi e gli errori vengono inviati a /dev/stderr , quindi dovrebbero essere visibili sulla console.

Per personalizzare i log (come la formattazione), prova il seguente esempio:

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

2

Quello che faccio in questi casi è avere un log.debug()messaggio con alcuni messaggi nella mia applicazione. Poiché il livello di registrazione predefinito èWARNING , tali messaggi non vengono visualizzati nella normale esecuzione.

Quindi, nell'unità, cambio il livello di registrazione in DEBUG, in modo che tali messaggi vengano visualizzati durante l'esecuzione.

import logging

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

Negli incontri:

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



Guarda un esempio completo:

Questa è daikiri.pyuna classe base che implementa un Daikiri con il suo nome e prezzo. C'è un metodo make_discount()che restituisce il prezzo di quel daikiri specifico dopo aver applicato un determinato sconto:

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

Quindi, creo un unittest test_daikiri.pyche controlla il suo utilizzo:

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

Quindi quando lo eseguo ricevo i log.debugmessaggi:

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

OK

1

inspect.trace ti permetterà di ottenere variabili locali dopo che è stata lanciata un'eccezione. È quindi possibile avvolgere gli unit test con un decoratore come il seguente per salvare quelle variabili locali per l'esame durante l'autopsia.

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

L'ultima riga stamperà i valori restituiti dove il test è riuscito e le variabili locali, in questo caso x, quando fallisce:

{'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 :-)


0

Che ne dici di catturare l'eccezione generata dall'errore di asserzione? Nel tuo blocco di cattura puoi inviare i dati come preferisci ovunque. Quindi, quando hai finito, potresti rilanciare l'eccezione. Il collaudatore probabilmente non saprebbe la differenza.

Dichiarazione di non responsabilità: non l'ho provato con il framework di unit test di Python ma l'ho fatto con altri framework di unit test.



-1

Espandendo la risposta di @FC, questo funziona abbastanza bene per me:

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),
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.