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.
Admitiendo que no lo he probado, la función de registro de las pruebas es bastante útil ...
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 :-)