Salida de datos de la prueba unitaria en Python

115

Si estoy escribiendo pruebas unitarias en python (usando el módulo unittest), ¿es posible generar datos de una prueba fallida, de modo que pueda examinarlos para ayudar a deducir qué causó el error? Soy consciente de la capacidad de crear un mensaje personalizado, que puede contener cierta información, pero a veces es posible que se trate de datos más complejos, que no se pueden representar fácilmente como una cadena.

Por ejemplo, suponga que tiene una clase Foo y está probando una barra de método, utilizando datos de una lista llamada testdata:

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

Si la prueba falló, es posible que desee generar t1, t2 y / o f, para ver por qué estos datos en particular dieron como resultado una falla. Por salida, me refiero a que se puede acceder a las variables como cualquier otra variable, después de que se haya ejecutado la prueba.

Lepisma
fuente

Respuestas:

73

Respuesta muy tardía para alguien que, como yo, viene aquí buscando una respuesta sencilla y rápida.

En Python 2.7, podría usar un parámetro adicional msgpara agregar información al mensaje de error como este:

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

Documentos oficiales aquí

Facundo Casco
fuente
1
También funciona en Python 3.
MrDBA
18
Los documentos sugieren esto, pero vale la pena mencionarlo explícitamente: de forma predeterminada, si msgse usa, reemplazará el mensaje de error normal. Para msgque se agregue al mensaje de error normal, también debe configurar TestCase.longMessage en True
Catalin Iacob
1
Es bueno saber que podemos pasar un mensaje de error personalizado, pero estaba interesado en imprimir algún mensaje independientemente del error.
Harry Moreno
5
El comentario de @CatalinIacob se aplica a Python 2.x. En Python 3.x, TestCase.longMessage tiene el valor predeterminado True.
ndmeiri
70

Usamos el módulo de registro para esto.

Por ejemplo:

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

Eso nos permite activar la depuración para pruebas específicas que sabemos que están fallando y para las que queremos información de depuración adicional.

Sin embargo, mi método preferido no es dedicar mucho tiempo a la depuración, sino dedicarlo a escribir pruebas más detalladas para exponer el problema.

S. Lot
fuente
¿Qué pasa si llamo a un método foo dentro de testSomething y registra algo? ¿Cómo puedo ver el resultado de eso sin pasar el registrador a foo?
simao
@simao: ¿Qué es foo? ¿Una función separada? ¿Una función de método de SomeTest? En el primer caso, una función puede tener su propio registrador. En el segundo caso, la otra función del método puede tener su propio registrador. ¿Conoce cómo funciona el loggingpaquete? Varios registradores son la norma.
S.Lott
8
Configuré el registro de la manera exacta que especificó. Supongo que está funcionando, pero ¿dónde veo el resultado? No se envía a la consola. Intenté configurarlo con el registro en un archivo, pero eso tampoco produce ningún resultado.
MikeyE
"Mi método preferido, sin embargo, no es dedicar mucho tiempo a depurar, sino dedicarlo a escribir pruebas más detalladas para exponer el problema". -- ¡bien dicho!
Seth
34

Puede utilizar declaraciones de impresión simples o cualquier otra forma de escribir en stdout. También puede invocar el depurador de Python en cualquier lugar de sus pruebas.

Si usa nose para ejecutar sus pruebas (que recomiendo), recopilará la salida estándar para cada prueba y solo se la mostrará si la prueba falló, por lo que no tiene que vivir con la salida desordenada cuando las pruebas pasan.

nose también tiene interruptores para mostrar automáticamente las variables mencionadas en afirmaciones, o para invocar al depurador en pruebas fallidas. Por ejemplo -s( --nocapture) evita la captura de stdout.

Ned Batchelder
fuente
Desafortunadamente, nose no parece recopilar el registro escrito en stdout / err usando el marco de registro. Tengo el printy log.debug()uno al lado del otro, y activo explícitamente el DEBUGregistro en la raíz desde el setUp()método, pero solo printaparece el resultado.
haridsv
7
nosetests -smuestra el contenido de stdout si hay un error o no, algo que encuentro útil.
hargriffle
No puedo encontrar los interruptores para mostrar automáticamente las variables en los documentos de nariz. ¿Puedes señalarme algo que los describa?
ABM
No conozco una forma de mostrar automáticamente las variables de nose o unittest. Imprimo las cosas que quiero ver en mis pruebas.
Ned Batchelder
16

No creo que esto sea exactamente lo que está buscando, no hay forma de mostrar valores de variables que no fallan, pero esto puede ayudarlo a acercarse a generar los resultados de la manera que desea.

Puede utilizar el objeto TestResult devuelto por TestRunner.run () para el análisis y procesamiento de resultados. En particular, TestResult.errors y TestResult.failures

Acerca del objeto TestResults:

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

Y algún código para apuntarle en la dirección correcta:

>>> 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
fuente
5

Otra opción: iniciar un depurador donde falla la prueba.

Intente ejecutar sus pruebas con Testoob (ejecutará su suite unittest sin cambios), y puede usar el interruptor de línea de comando '--debug' para abrir un depurador cuando una prueba falla.

Aquí hay una sesión de terminal en 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
fuente
2
Nose ( nose.readthedocs.org/en/latest/index.html ) es otro marco que proporciona opciones para "iniciar una sesión de depuración". Lo ejecuto con '-sx --pdb --pdb-failures', que no consume la salida, se detiene después del primer error y cae en pdb en excepciones y errores de prueba. Esto ha eliminado mi necesidad de mensajes de error enriquecidos, a menos que sea vago y pruebe en un bucle.
jwhitlock
5

El método que utilizo es realmente sencillo. Simplemente lo registro como una advertencia para que realmente aparezca.

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
fuente
¿Funcionará esto si la prueba tiene éxito? En mi caso, la advertencia se muestra solo si la prueba falla
Shreya Maria
@ShreyaMaria sí lo hará
Orane
5

Creo que podría haber estado pensando demasiado en esto. Una forma en que se me ocurrió que funciona, es simplemente tener una variable global, que acumule los datos de diagnóstico.

Algo como esto:

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

Gracias por las respuestas. Me han dado algunas ideas alternativas sobre cómo registrar información de pruebas unitarias en Python.

Lepisma
fuente
2

Usar registro:

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

Si no lo configura LOG_FILE, el registro llegará a stderr.

no-un-usuario
fuente
2

Puedes usar el loggingmódulo para eso.

Entonces, en el código de prueba unitaria, use:

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

De forma predeterminada, las advertencias y los errores se envían a /dev/stderr, por lo que deben estar visibles en la consola.

Para personalizar los registros (como el formato), pruebe el siguiente ejemplo:

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

Lo que hago en estos casos es tener un log.debug()mensaje con algunos mensajes en mi aplicación. Dado que el nivel de registro predeterminado es WARNING, estos mensajes no se muestran en la ejecución normal.

Luego, en la prueba unitaria, cambio el nivel de registro a DEBUG, para que dichos mensajes se muestren mientras se ejecutan.

import logging

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

En las pruebas unitarias:

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



Vea un ejemplo completo:

Es decir daikiri.py, una clase básica que implementa un Daikiri con su nombre y precio. Existe un método make_discount()que devuelve el precio de ese daikiri específico después de aplicar un descuento determinado:

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

Luego, creo una prueba unitaria test_daikiri.pyque verifica su uso:

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

Entonces, cuando lo ejecuto, obtengo los log.debugmensajes:

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

OK
fedorqui 'ASÍ que deja de hacer daño'
fuente
1

inspect.trace le permitirá obtener variables locales después de que se haya lanzado una excepción. A continuación, puede envolver las pruebas unitarias con un decorador como el siguiente para guardar esas variables locales para su examen durante la 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))

La última línea imprimirá los valores devueltos donde la prueba tuvo éxito y las variables locales, en este caso x, cuando falla:

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

¿Qué tal detectar la excepción que se genera a partir del error de aserción? En su bloque de captura, podría generar los datos como quisiera en cualquier lugar. Luego, cuando haya terminado, puede volver a lanzar la excepción. El corredor de pruebas probablemente no notaría la diferencia.

Descargo de responsabilidad: no he probado esto con el marco de prueba unitario de Python, pero lo he hecho con otros marcos de prueba unitaria.

Sam Corder
fuente
-1

Ampliando la respuesta de @FC, esto funciona bastante bien para mí:

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
fuente