EDITAR: cambió a un mejor ejemplo y aclaró por qué esto es un problema real.
Me gustaría escribir pruebas unitarias en Python que continúen ejecutándose cuando falla una afirmación, para poder ver múltiples fallas en una sola prueba. Por ejemplo:
class Car(object):
def __init__(self, make, model):
self.make = make
self.model = make # Copy and paste error: should be model.
self.has_seats = True
self.wheel_count = 3 # Typo: should be 4.
class CarTest(unittest.TestCase):
def test_init(self):
make = "Ford"
model = "Model T"
car = Car(make=make, model=model)
self.assertEqual(car.make, make)
self.assertEqual(car.model, model) # Failure!
self.assertTrue(car.has_seats)
self.assertEqual(car.wheel_count, 4) # Failure!
Aquí, el propósito de la prueba es asegurarse de que Car's __init__
configure sus campos correctamente. Podría dividirlo en cuatro métodos (y eso suele ser una gran idea), pero en este caso creo que es más legible mantenerlo como un método único que prueba un concepto único ("el objeto se inicializa correctamente").
Si asumimos que es mejor no dividir el método, entonces tengo un nuevo problema: no puedo ver todos los errores a la vez. Cuando soluciono el model
error y vuelvo a ejecutar la prueba, wheel_count
aparece el error. Me ahorraría tiempo ver ambos errores cuando ejecute la prueba por primera vez.
A modo de comparación, el marco de pruebas unitarias C ++ de Google distingue entreEXPECT_*
afirmaciones no fatales y ASSERT_*
afirmaciones fatales :
Las afirmaciones vienen en pares que prueban lo mismo pero tienen diferentes efectos en la función actual. Las versiones ASSERT_ * generan fallas fatales cuando fallan y abortan la función actual. Las versiones EXPECT_ * generan fallas no fatales, que no abortan la función actual. Por lo general, se prefieren EXPECT_ *, ya que permiten informar más de una falla en una prueba. Sin embargo, debe usar ASSERT_ * si no tiene sentido continuar cuando falla la afirmación en cuestión.
¿Hay alguna manera de obtener un EXPECT_*
comportamiento similar en Python unittest
? Si no está dentro unittest
, ¿hay otro marco de prueba unitario de Python que admita este comportamiento?
Por cierto, tenía curiosidad acerca de cuántas pruebas de la vida real podrían beneficiarse de afirmaciones no fatales, así que miré algunos ejemplos de código (editado 2014-08-19 para usar el código de búsqueda en lugar de Google Code Search, RIP). De los 10 resultados seleccionados al azar de la primera página, todos contenían pruebas que realizaban múltiples afirmaciones independientes en el mismo método de prueba. Todos se beneficiarían de afirmaciones no fatales.
fuente
Respuestas:
Lo que probablemente querrá hacer es derivar,
unittest.TestCase
ya que esa es la clase que lanza cuando falla una aserción. Tendrá que rediseñar suTestCase
para no lanzar (tal vez mantenga una lista de fallas). Rediseñar las cosas puede causar otros problemas que tendrías que resolver. Por ejemplo, puede terminar necesitando derivarTestSuite
para realizar cambios en apoyo de los cambios realizados en suTestCase
.fuente
TestCase
por el bien de implementar afirmaciones suaves; son especialmente fáciles de hacer en Python: simplemente capture todos susAssertionError
s (tal vez en un bucle simple) y guárdelos en una lista o un conjunto , luego reprobarlos todos a la vez. Consulte la respuesta de @Anthony Batchelor para obtener más detalles.Otra forma de tener aserciones no fatales es capturar la excepción de aserción y almacenar las excepciones en una lista. Luego afirme que esa lista está vacía como parte del tearDown.
import unittest class Car(object): def __init__(self, make, model): self.make = make self.model = make # Copy and paste error: should be model. self.has_seats = True self.wheel_count = 3 # Typo: should be 4. class CarTest(unittest.TestCase): def setUp(self): self.verificationErrors = [] def tearDown(self): self.assertEqual([], self.verificationErrors) def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) try: self.assertEqual(car.make, make) except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertEqual(car.model, model) # Failure! except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertTrue(car.has_seats) except AssertionError, e: self.verificationErrors.append(str(e)) try: self.assertEqual(car.wheel_count, 4) # Failure! except AssertionError, e: self.verificationErrors.append(str(e)) if __name__ == "__main__": unittest.main()
fuente
unittest.TestCase
con bloques try / except.Una opción es hacer valer todos los valores a la vez como una tupla.
Por ejemplo:
class CarTest(unittest.TestCase): def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) self.assertEqual( (car.make, car.model, car.has_seats, car.wheel_count), (make, model, True, 4))
El resultado de estas pruebas sería:
====================================================================== FAIL: test_init (test.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\temp\py_mult_assert\test.py", line 17, in test_init (make, model, True, 4)) AssertionError: Tuples differ: ('Ford', 'Ford', True, 3) != ('Ford', 'Model T', True, 4) First differing element 1: Ford Model T - ('Ford', 'Ford', True, 3) ? ^ - ^ + ('Ford', 'Model T', True, 4) ? ^ ++++ ^
Esto muestra que tanto el modelo como el número de ruedas son incorrectos.
fuente
Se considera un anti-patrón tener múltiples afirmaciones en una sola prueba unitaria. Se espera que una prueba de una sola unidad pruebe solo una cosa. Quizás esté probando demasiado. Considere dividir esta prueba en varias pruebas. De esta manera, puede nombrar cada prueba correctamente.
A veces, sin embargo, está bien comprobar varias cosas al mismo tiempo. Por ejemplo, cuando afirma propiedades del mismo objeto. En ese caso, de hecho está afirmando si ese objeto es correcto. Una forma de hacer esto es escribir un método auxiliar personalizado que sepa cómo hacer valer en ese objeto. Puede escribir ese método de tal manera que muestre todas las propiedades defectuosas o, por ejemplo, muestre el estado completo del objeto esperado y el estado completo del objeto real cuando falla una aserción.
fuente
Desde Python 3.4 también puede usar subpruebas :
def test_init(self): make = "Ford" model = "Model T" car = Car(make=make, model=model) with self.subTest(msg='Car.make check'): self.assertEqual(car.make, make) with self.subTest(msg='Car.model check'): self.assertEqual(car.model, model) with self.subTest(msg='Car.has_seats check'): self.assertTrue(car.has_seats) with self.subTest(msg='Car.wheel_count check'): self.assertEqual(car.wheel_count, 4)
(el
msg
parámetro se usa para determinar más fácilmente qué prueba falló).Salida:
====================================================================== FAIL: test_init (__main__.CarTest) [Car.model check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 23, in test_init self.assertEqual(car.model, model) AssertionError: 'Ford' != 'Model T' - Ford + Model T ====================================================================== FAIL: test_init (__main__.CarTest) [Car.wheel_count check] ---------------------------------------------------------------------- Traceback (most recent call last): File "test.py", line 27, in test_init self.assertEqual(car.wheel_count, 4) AssertionError: 3 != 4 ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=2)
fuente
Haga cada afirmación en un método separado.
class MathTest(unittest.TestCase): def test_addition1(self): self.assertEqual(1 + 0, 1) def test_addition2(self): self.assertEqual(1 + 1, 3) def test_addition3(self): self.assertEqual(1 + (-1), 0) def test_addition4(self): self.assertEqaul(-1 + (-1), -1)
fuente
setup()
, porque esa es una de las pruebas. Pero si pongo cada afirmación en su propia función, entonces tengo que cargar datos 3 veces, y eso es un gran desperdicio de recursos. ¿Cuál es la mejor manera de lidiar con una situación como esa?Hay un paquete de aserción suave en PyPI llamado
softest
que manejará sus requisitos. Funciona recopilando las fallas, combinando excepciones y datos de seguimiento de pila, e informando todo como parte de launittest
salida habitual .Por ejemplo, este código:
import softest class ExampleTest(softest.TestCase): def test_example(self): # be sure to pass the assert method object, not a call to it self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle') # self.soft_assert(self.assertEqual('Worf', 'wharf', 'Klingon is not ship receptacle')) # will not work as desired self.soft_assert(self.assertTrue, True) self.soft_assert(self.assertTrue, False) self.assert_all() if __name__ == '__main__': softest.main()
... produce esta salida de consola:
====================================================================== FAIL: "test_example" (ExampleTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "C:\...\softest_test.py", line 14, in test_example self.assert_all() File "C:\...\softest\case.py", line 138, in assert_all self.fail(''.join(failure_output)) AssertionError: ++++ soft assert failure details follow below ++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ The following 2 failures were found in "test_example" (ExampleTest): ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Failure 1 ("test_example" method) +--------------------------------------------------------------------+ Traceback (most recent call last): File "C:\...\softest_test.py", line 10, in test_example self.soft_assert(self.assertEqual, 'Worf', 'wharf', 'Klingon is not ship receptacle') File "C:\...\softest\case.py", line 84, in soft_assert assert_method(*arguments, **keywords) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 829, in assertEqual assertion_func(first, second, msg=msg) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 1203, in assertMultiLineEqual self.fail(self._formatMessage(msg, standardMsg)) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 670, in fail raise self.failureException(msg) AssertionError: 'Worf' != 'wharf' - Worf + wharf : Klingon is not ship receptacle +--------------------------------------------------------------------+ Failure 2 ("test_example" method) +--------------------------------------------------------------------+ Traceback (most recent call last): File "C:\...\softest_test.py", line 12, in test_example self.soft_assert(self.assertTrue, False) File "C:\...\softest\case.py", line 84, in soft_assert assert_method(*arguments, **keywords) File "C:\...\Python\Python36-32\lib\unittest\case.py", line 682, in assertTrue raise self.failureException(msg) AssertionError: False is not true ---------------------------------------------------------------------- Ran 1 test in 0.000s FAILED (failures=1)
NOTA : Creé y mantengo
softest
.fuente
esperar es muy útil en gtest. Esta es la forma de Python en esencia y código:
import sys import unittest class TestCase(unittest.TestCase): def run(self, result=None): if result is None: self.result = self.defaultTestResult() else: self.result = result return unittest.TestCase.run(self, result) def expect(self, val, msg=None): ''' Like TestCase.assert_, but doesn't halt the test. ''' try: self.assert_(val, msg) except: self.result.addFailure(self, sys.exc_info()) def expectEqual(self, first, second, msg=None): try: self.failUnlessEqual(first, second, msg) except: self.result.addFailure(self, sys.exc_info()) expect_equal = expectEqual assert_equal = unittest.TestCase.assertEqual assert_raises = unittest.TestCase.assertRaises test_main = unittest.main
fuente
Me gustó el enfoque de @ Anthony-Batchelor para capturar la excepción AssertionError. Pero una ligera variación de este enfoque utilizando decoradores y también una forma de informar los casos de prueba con pasa / falla.
#!/usr/bin/env python # -*- coding: utf-8 -*- import unittest class UTReporter(object): ''' The UT Report class keeps track of tests cases that have been executed. ''' def __init__(self): self.testcases = [] print "init called" def add_testcase(self, testcase): self.testcases.append(testcase) def display_report(self): for tc in self.testcases: msg = "=============================" + "\n" + \ "Name: " + tc['name'] + "\n" + \ "Description: " + str(tc['description']) + "\n" + \ "Status: " + tc['status'] + "\n" print msg reporter = UTReporter() def assert_capture(*args, **kwargs): ''' The Decorator defines the override behavior. unit test functions decorated with this decorator, will ignore the Unittest AssertionError. Instead they will log the test case to the UTReporter. ''' def assert_decorator(func): def inner(*args, **kwargs): tc = {} tc['name'] = func.__name__ tc['description'] = func.__doc__ try: func(*args, **kwargs) tc['status'] = 'pass' except AssertionError: tc['status'] = 'fail' reporter.add_testcase(tc) return inner return assert_decorator class DecorateUt(unittest.TestCase): @assert_capture() def test_basic(self): x = 5 self.assertEqual(x, 4) @assert_capture() def test_basic_2(self): x = 4 self.assertEqual(x, 4) def main(): #unittest.main() suite = unittest.TestLoader().loadTestsFromTestCase(DecorateUt) unittest.TextTestRunner(verbosity=2).run(suite) reporter.display_report() if __name__ == '__main__': main()
Salida de la consola:
(awsenv)$ ./decorators.py init called test_basic (__main__.DecorateUt) ... ok test_basic_2 (__main__.DecorateUt) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK ============================= Name: test_basic Description: None Status: fail ============================= Name: test_basic_2 Description: None Status: pass
fuente
Tuve un problema con la respuesta de @Anthony Batchelor porque me habría obligado a usar
try...catch
dentro de mis pruebas unitarias. En cambio, encapsulé latry...catch
lógica en una anulación delTestCase.assertEqual
método. Aquí está el código:import unittest import traceback class AssertionErrorData(object): def __init__(self, stacktrace, message): super(AssertionErrorData, self).__init__() self.stacktrace = stacktrace self.message = message class MultipleAssertionFailures(unittest.TestCase): def __init__(self, *args, **kwargs): self.verificationErrors = [] super(MultipleAssertionFailures, self).__init__( *args, **kwargs ) def tearDown(self): super(MultipleAssertionFailures, self).tearDown() if self.verificationErrors: index = 0 errors = [] for error in self.verificationErrors: index += 1 errors.append( "%s\nAssertionError %s: %s" % ( error.stacktrace, index, error.message ) ) self.fail( '\n\n' + "\n".join( errors ) ) self.verificationErrors.clear() def assertEqual(self, goal, results, msg=None): try: super( MultipleAssertionFailures, self ).assertEqual( goal, results, msg ) except unittest.TestCase.failureException as error: goodtraces = self._goodStackTraces() self.verificationErrors.append( AssertionErrorData( "\n".join( goodtraces[:-2] ), error ) ) def _goodStackTraces(self): """ Get only the relevant part of stacktrace. """ stop = False found = False goodtraces = [] # stacktrace = traceback.format_exc() # stacktrace = traceback.format_stack() stacktrace = traceback.extract_stack() # /programming/54499367/how-to-correctly-override-testcase for stack in stacktrace: filename = stack.filename if found and not stop and \ not filename.find( 'lib' ) < filename.find( 'unittest' ): stop = True if not found and filename.find( 'lib' ) < filename.find( 'unittest' ): found = True if stop and found: stackline = ' File "%s", line %s, in %s\n %s' % ( stack.filename, stack.lineno, stack.name, stack.line ) goodtraces.append( stackline ) return goodtraces # class DummyTestCase(unittest.TestCase): class DummyTestCase(MultipleAssertionFailures): def setUp(self): self.maxDiff = None super(DummyTestCase, self).setUp() def tearDown(self): super(DummyTestCase, self).tearDown() def test_function_name(self): self.assertEqual( "var", "bar" ) self.assertEqual( "1937", "511" ) if __name__ == '__main__': unittest.main()
Salida de resultado:
F ====================================================================== FAIL: test_function_name (__main__.DummyTestCase) ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\User\Downloads\test.py", line 77, in tearDown super(DummyTestCase, self).tearDown() File "D:\User\Downloads\test.py", line 29, in tearDown self.fail( '\n\n' + "\n\n".join( errors ) ) AssertionError: File "D:\User\Downloads\test.py", line 80, in test_function_name self.assertEqual( "var", "bar" ) AssertionError 1: 'var' != 'bar' - var ? ^ + bar ? ^ : File "D:\User\Downloads\test.py", line 81, in test_function_name self.assertEqual( "1937", "511" ) AssertionError 2: '1937' != '511' - 1937 + 511 :
Se pueden publicar más soluciones alternativas para la captura correcta de seguimiento de pila en ¿Cómo anular correctamente TestCase.assertEqual (), produciendo el seguimiento de pila correcto?
fuente
No creo que haya una forma de hacer esto con PyUnit y no me gustaría ver PyUnit extendido de esta manera.
Prefiero ceñirme a una afirmación por función de prueba ( o más específicamente afirmar un concepto por prueba ) y reescribiría
test_addition()
como cuatro funciones de prueba separadas. Esto brindaría información más útil sobre fallas, a saber :.FF. ====================================================================== FAIL: test_addition_with_two_negatives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 10, in test_addition_with_two_negatives self.assertEqual(-1 + (-1), -1) AssertionError: -2 != -1 ====================================================================== FAIL: test_addition_with_two_positives (__main__.MathTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_addition.py", line 6, in test_addition_with_two_positives self.assertEqual(1 + 1, 3) # Failure! AssertionError: 2 != 3 ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=2)
Si decide que este enfoque no es para usted, puede encontrar útil esta respuesta .
Actualizar
Parece que está probando dos conceptos con su pregunta actualizada y los dividiría en dos pruebas unitarias. El primero es que los parámetros se almacenan en la creación de un nuevo objeto. Esto tendría dos afirmaciones, una para
make
y otra paramodel
. Si el primero falla, lo que claramente debe arreglarse, si el segundo pasa o falla es irrelevante en este momento.El segundo concepto es más cuestionable ... Está probando si se inicializan algunos valores predeterminados. ¿Por qué ? Sería más útil probar estos valores en el punto en que realmente se utilizan (y si no se utilizan, ¿por qué están ahí?).
Ambas pruebas fallan, y ambas deberían. Cuando estoy haciendo pruebas unitarias, estoy mucho más interesado en el fracaso que en el éxito, ya que ahí es donde necesito concentrarme.
FF ====================================================================== FAIL: test_creation_defaults (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 25, in test_creation_defaults self.assertEqual(self.car.wheel_count, 4) # Failure! AssertionError: 3 != 4 ====================================================================== FAIL: test_creation_parameters (__main__.CarTest) ---------------------------------------------------------------------- Traceback (most recent call last): File "test_car.py", line 20, in test_creation_parameters self.assertEqual(self.car.model, self.model) # Failure! AssertionError: 'Ford' != 'Model T' ---------------------------------------------------------------------- Ran 2 tests in 0.000s FAILED (failures=2)
fuente
Me doy cuenta de que esta pregunta se hizo literalmente hace años, pero ahora hay (al menos) dos paquetes de Python que le permiten hacer esto.
Uno es el más suave: https://pypi.org/project/softest/
El otro es Python-Delayed-Assert: https://github.com/pr4bh4sh/python-delayed-assert
Yo tampoco los he usado, pero se parecen bastante a mí.
fuente