integracion - Continuar en la prueba unitaria de Python cuando falla una afirmación
pruebas de integracion python (9)
Hacer 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)
EDITAR: cambió a un mejor ejemplo, y aclaró por qué este es un problema real.
Me gustaría escribir pruebas unitarias en Python que continúen ejecutándose cuando falla una aserción, de modo que pueda 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 objetivo de la prueba es garantizar que __init__
de __init__
establece correctamente sus campos. Podría dividirlo en cuatro métodos (y eso a menudo es una gran idea), pero en este caso creo que es más legible mantenerlo como un único método que prueba un solo concepto ("el objeto se inicializa correctamente").
Si suponemos que es mejor no romper el método, entonces tengo un nuevo problema: no puedo ver todos los errores a la vez. Cuando soluciono el error del model
y vuelvo a ejecutar la prueba, aparece el error wheel_count
. Me ahorraría tiempo para ver ambos errores la primera vez que ejecute la prueba.
A modo de comparación, el marco de prueba de la unidad C ++ de Google distingue entre las aserciones EXPECT_*
no fatales y las aserciones fatales ASSERT_*
:
Las aserciones 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 cancelan la función actual. Las versiones EXPECT_ * generan fallas no fatales, que no abortan la función actual. Normalmente se prefiere 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 forma de obtener el comportamiento EXPECT_*
de Python? Si no está en la prueba de unidad, ¿existe otro marco de prueba de unidad de Python que admita este comportamiento?
Incidentalmente, tenía curiosidad sobre cuántas pruebas de la vida real podrían beneficiarse de las afirmaciones no fatales, así que miré algunos ejemplos de código (editados 2014-08-19 para usar searchcode 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 las afirmaciones no fatales.
Hay un paquete de aserción suave en PyPI llamado softest
que manejará sus requisitos. Funciona mediante la recopilación de fallas, combinando datos de rastreo de excepción y de pila, y reportándolo todo como parte del resultado habitual de la unittest
.
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 softest
.
Lo que probablemente querrás hacer es derivar unittest.TestCase
ya que esa es la clase que arroja cuando falla una afirmación. Tendrá que volver a TestCase
su TestCase
para no tirar (quizás mantenga una lista de fallas en su lugar). Volver a diseñar cosas puede causar otros problemas que tendría que resolver. Por ejemplo, puede terminar necesitando derivar TestSuite
para realizar cambios en apoyo de los cambios realizados en su TestCase
.
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 aprobado / reprobado.
#!/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
No creo que haya una forma de hacer esto con PyUnit y no me gustaría ver que PyUnit se extienda de esta manera.
Prefiero apegarme a una afirmación por función de prueba ( o más específicamente afirmar un concepto por prueba ) y volver a escribir test_addition()
como cuatro funciones de prueba separadas. Esto daría más información útil sobre el fracaso, 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 esta respuesta útil.
Actualizar
Parece que estás probando dos conceptos con tu 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 para model
. Si el primero falla, el que claramente necesita ser reparado, ya sea que el segundo pase o falle es irrelevante en este momento.
El segundo concepto es más cuestionable ... Está probando si algunos valores predeterminados se inicializan. Por qué ? Sería más útil probar estos valores en el punto en que realmente se usan (y si no se usan, ¿por qué están allí?).
Ambas pruebas fallan, y ambas deberían. Cuando estoy realizando pruebas unitarias, estoy mucho más interesado en el fracaso que en el éxito ya que es allí donde debo 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)
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 de la función 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()
Se considera un antipatrón para 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ás probando demasiado. Considere dividir esta prueba en múltiples pruebas. De esta manera puede nombrar cada prueba de manera adecuada.
A veces, sin embargo, está bien verificar varias cosas al mismo tiempo. Por ejemplo, cuando está afirmando propiedades del mismo objeto. En ese caso, de hecho estás afirmando si ese objeto es correcto. Una forma de hacerlo es escribir un método de ayuda personalizado que sepa cómo afirmar en ese objeto. Puede escribir ese método de forma que muestre todas las propiedades anómalas o, por ejemplo, muestre el estado completo del objeto esperado y el estado completo del objeto real cuando falla una afirmación.
Una opción es afirmar en 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 conteo de ruedas son incorrectos.
esperar es muy útil en gtest. Esta es la forma de python en gist , y el 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