Pregunta

EDIT:. Conmuta a un mejor ejemplo, y aclaró por qué esto es un problema real

Me gustaría escribir pruebas unitarias en Python que continuar con la ejecución cuando una aserción falla, por lo que puedo ver varios errores 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 asegurar que __init__ de coche establece sus campos correctamente. Podría dividirla en cuatro métodos (y eso es a menudo una gran idea), pero en este caso creo que es más fácil de leer para mantenerlo como un único método que pone a prueba un solo concepto ( "el objeto se ha inicializado correctamente").

Si asumimos que es mejor para no romper el método, entonces tengo un nuevo problema: no puedo ver todos los errores a la vez. Cuando arreglo el error model y vuelva a ejecutar la prueba, entonces se aparece el error wheel_count. Me ahorraría tiempo de ver los dos errores cuando ejecuta por primera vez la prueba.

Para la comparación, de Google C ++ marco de pruebas de unidad distingue entre entre las afirmaciones no mortales EXPECT_* y afirmaciones ASSERT_* fatales:

Las afirmaciones que vienen en pares prueba de lo mismo, pero tienen diferentes efectos sobre la función actual. ASSERT_ * versiones generan fallas fatales cuando fallan, y abortar la función actual. EXPECT_ * versiones generan fallos no fatales, que no hacen abortar la función actual. Por lo general, EXPECT_ * son preferidos, ya que permiten que más de uno fallos que se informó en un ensayo. Sin embargo, se debe utilizar ASSERT_ * si no tiene sentido para continuar cuando la afirmación de que se trate no.

¿Hay una manera de conseguir EXPECT_*-como el comportamiento en unittest de Python? Si no está en unittest, entonces hay otro marco de prueba de unidad de Python que es compatible con este comportamiento?


Por cierto, yo tenía curiosidad acerca de cuántas pruebas de la vida real podría beneficiarse de las afirmaciones no fatales, así que busqué en algún ejemplos de código (editado 19/08/2014 al uso searchcode en lugar de Google Code Search, RIP). Fuera de 10 resultados seleccionados al azar de la primera página, todos contenían pruebas que hicieron múltiples afirmaciones independientes en el mismo método de ensayo. Todos se beneficiarían de afirmaciones no fatales.

¿Fue útil?

Solución

Lo que usted probablemente querrá hacer es unittest.TestCase derivan ya que es la clase que lanza cuando falla una aserción. Usted tendrá que rediseñar su TestCase no tirar (tal vez mantener una lista de fracasos en su lugar). Re-arquitectura de cosas pueden causar otros problemas que habría que resolver. Por ejemplo, usted puede terminar encima de necesitar TestSuite Derivar para realizar cambios en el apoyo de los cambios realizados en su TestCase.

Otros consejos

Otra manera de tener afirmaciones no fatales es capturar la excepción afirmación y almacenar las excepciones en una lista. A continuación, afirman que la lista está vacía, como parte de la 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()

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

La salida de este 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 demuestra que tanto el modelo como el recuento de la rueda son incorrectos.

Se considera un anti-patrón para tener múltiples afirma en una sola unidad de prueba. Se espera que una prueba de unidad única para poner a prueba una sola cosa. Tal vez usted está probando demasiado. Considere dividir esta prueba en múltiples pruebas. De esta manera usted puede nombrar cada prueba correctamente.

A veces sin embargo, que está bien para comprobar varias cosas al mismo tiempo. Por ejemplo, cuando se está afirmando propiedades del mismo objeto. En ese caso, usted es, de hecho, afirmando que si el objeto es correcta. Una manera de hacer esto es escribir un método de ayuda personalizada que sabe cómo hacer valer en ese objeto. Se puede escribir que el método de tal manera que muestra todas las propiedades que fallan o, por ejemplo muestra el estado completo del objeto esperado y el estado completo del objeto real cuando una aserción falla.

Haga cada aserció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)

Me gusta el enfoque por @ Anthony-Batchelor, para capturar la excepción AssertionError. Sin embargo, una ligera variación de este enfoque utilizando decoradores y también una manera de informar de los casos de prueba con pasa / no pasa.

#!/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()

La 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

esperar es muy útil en GTEST. Esta es la forma de pitón 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

Hay un paquete afirmación suave en PyPI llama softest que se encargará de sus necesidades. Funciona mediante la recopilación de los fracasos, la combinación de datos de excepción y el seguimiento de la pila, e informar todo como parte de la salida unittest 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 la 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: : I creado y mantienen softest

No creo que hay una manera de hacer esto con PyUnit y no querría ver PyUnit extendió de esta manera.

Yo prefiero que se adhieren a una afirmación por la función de prueba ( o más afirmando un concepto específicamente por prueba ) y que reescribir test_addition() como cuatro funciones de prueba independientes. Esto daría una información más útil en caso de fallo, 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, usted puede encontrar esta respuesta útiles.

Actualizar

Parece que se está probando dos conceptos con su pregunta actualizada y sin dividir éstas en dos pruebas unitarias. La primera es que los parámetros se almacenan en la creación de un nuevo objeto. Esto tendría dos afirmaciones, uno para make y uno para model. Si el primero falla, el que claramente necesita ser fijado, si el segundo pasa o no es irrelevante en esta coyuntura.

El segundo concepto es más cuestionable ... que está probando si algunos valores por defecto se inicializan. ¿Por qué ? Sería más útil para poner a prueba estos valores en el punto que se utilizan en realidad (y si no se utilizan, entonces ¿por qué están ahí?).

Estas dos pruebas fallan, y ambos deben. Cuando estoy unidad de pruebas, estoy mucho más interesado en el fracaso de lo que soy en el éxito ya que es donde necesito concentrado.

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)

Tengo un problema con la respuesta @Anthony Batchelor , porque obliga-yo use try...catch dentro de mis pruebas unitarias. Entonces, I encapsulado la lógica try...catch en una anulación del método TestCase.assertEqual. Este truco eliminar los bloques try...catch de la Unidad de Pruebas de 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()

        # https://stackoverflow.com/questions/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 del 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
 : 

Más soluciones alternativas para la captura StackTrace correcta podría ser publicado en Cómo reemplazar correctamente TestCase.assertEqual (), produciendo el StackTrace derecho?

Me di cuenta que pregunta se hizo, literalmente, hace años, pero ahora hay (al menos) dos paquetes de Python que le permiten hacer esto.

Uno es más suave: https://pypi.org/project/softest/

El otro es Python-Delayed-Assert: https://github.com/pr4bh4sh/ pitón instantáneo de afirmar

No he utilizado, ya sea, pero tienen un aspecto muy similar a mí.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top