Salida de datos de prueba unitaria en python

Si estoy escribiendo pruebas unitarias en python (usando el módulo unittest), ¿es posible generar datos de una prueba fallida, así que puedo examinarlo para ayudar a deducir qué causó el error? Soy consciente de la capacidad de crear un mensaje personalizado, que puede transmitir cierta información, pero a veces puede tratar con datos más complejos, que no pueden representarse fácilmente como una cadena.

Por ejemplo, supongamos que tienes un Foo de clase y estabas probando una barra de método, usando 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 un error. Por salida, quiero decir que se puede acceder a las variables como cualquier otra variable, después de que se haya ejecutado la prueba.

Respuesta muy tardía para alguien que, como yo, viene aquí en busca de una respuesta simple y rápida.

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

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

Docs oficiales aqui

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 cuales queremos información de depuración adicional.

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

Puede usar declaraciones impresas simples, o cualquier otra forma de escribir a stdout. También puede invocar el depurador de Python en cualquier parte de sus pruebas.

Si usa la nariz para ejecutar sus pruebas (que yo recomiendo), recostackrá el stdout para cada prueba y solo se lo mostrará si la prueba falló, por lo que no tiene que vivir con la salida abarrotada cuando pasan las pruebas.

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

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

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

Sobre el objeto TestResults:

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

Y algún código para que apuntes 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 "", line 11, in testchoice ZeroDivisionError: integer division or modulo by zero ====================================================================== FAIL: testshuffle (__main__.TestSequenceFunctions) ---------------------------------------------------------------------- Traceback (most recent call last): File "", 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 "" , 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 " ", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')] >>> 

Creo que podría haber estado pensando demasiado en esto. Una forma en la que he encontrado que hace el trabajo, es simplemente tener una variable global, que acumule los datos de diagnóstico.

Algo así:

 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 la información de las pruebas unitarias.

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

Intente ejecutar sus pruebas con Testoob (ejecutará su conjunto de unidades de prueba sin cambios), y puede usar el interruptor de línea de comando ‘–debug’ para abrir un depurador cuando falla una prueba.

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) 

El método que uso es muy simple. Solo 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) 

Utilice el 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 establece LOG_FILE , el registro se realizará en stderr .

Puedes usar el módulo de logging para eso.

Así que en el código de prueba de la unidad, 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.") 

Por defecto, las advertencias y los errores se envían a /dev/stderr , por lo que deberían 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) 

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

Luego, en el test de unidad, cambio el nivel de registro a DEBUG , para que se muestren dichos mensajes mientras se ejecutan.

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

En las pruebas de unidad:

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



Vea un ejemplo completo:

Esto es daikiri.py , una clase básica que implementa un Daikiri con su nombre y precio. Existe el 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 un test_daikiri.py unittest que 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() 

Así que cuando lo ejecuto obtengo los mensajes log.debug :

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

inspect.trace le permitirá obtener variables locales después de que se haya lanzado una excepción. Luego puede envolver las pruebas unitarias con un decorador como el siguiente para guardar esas variables locales para examinarlas 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 🙂

¿Qué hay de capturar la excepción que se genera a partir del error de aserción? En el bloque de captura, puede generar los datos como desee en cualquier lugar. Luego, cuando terminaste, puedes volver a lanzar la excepción. El corredor de pruebas probablemente no sabría la diferencia.

Descargo de responsabilidad: no he intentado esto con el marco de prueba de unidad de python, pero tengo con otros marcos de prueba de unidad

Admitiendo que no lo he probado, la función de registro de testfixtures parece bastante útil …

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