unittest unitarias pruebas integracion python unit-testing

unitarias - Salida de datos de la prueba unitaria en python



unittest python (14)

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

Por ejemplo, supongamos que tiene una clase Foo y está probando una barra de métodos, 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 falla, me gustaría dar salida a t1, t2 y / o f, para ver por qué esta información en particular resultó en una falla. Por salida, quiero decir que se puede acceder a las variables como cualquier otra variable, después de que se haya ejecutado la prueba.


¿Qué hay de la captura de la excepción que se genera a partir de la falla de aserción? En tu bloque de captura, puedes mostrar los datos como quieras en cualquier parte. Luego, cuando hayas terminado, podrás volver a lanzar la excepción. El corredor de prueba probablemente no sabría la diferencia.

Descargo de responsabilidad: No he intentado esto con el marco de prueba de unidades de Python, pero sí con otros marcos de pruebas de unidades.



Creo que podría haber estado pensando demasiado en esto. Una de las maneras en que se me ocurrió 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 el resplies. Me han dado algunas ideas alternativas sobre cómo registrar la información de las pruebas unitarias.


El método que uso es realmente simple. Acabo de registrarlo como una advertencia para que 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)


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


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 , dichos 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 al ejecutarlos.

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

En los unittest:

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

Vea un ejemplo completo:

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

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 test_daikiri.py 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()

Entonces, cuando lo ejecuto, recibo los mensajes de log.debug :

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


No creo que esto sea exactamente lo que estás buscando, no hay forma de mostrar los valores variables que no fallan, pero esto puede ayudarte a acercarte a la salida de los resultados de la forma que desees.

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

Acerca del objeto TestResults:

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

Y un código que lo guiará 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'')] >>>


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


Puede usar declaraciones simples de impresión 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), recogerá el stdout para cada prueba y solo se lo mostrará si la prueba falló, por lo que no tendrá que vivir con la salida desordenada cuando pasen las pruebas.

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


Puede usar el módulo de logging para eso.

Entonces, en el código de prueba de 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 deben estar visibles en la consola.

Para personalizar los registros (como el formateo), pruebe la siguiente muestra:

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


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

Documentos oficiales here


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.

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


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 configura LOG_FILE , el logging llegará a stderr .


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