unitarias - testing en python
Obteniendo los resultados de prueba unitarios de Python en un método tearDown() (10)
¿Es posible obtener los resultados de una prueba (es decir, si todas las aserciones han pasado) en un método tearDown ()? Estoy ejecutando scripts de Selenium, y me gustaría hacer algunos informes desde dentro de tearDown (), sin embargo, no sé si esto es posible.
Aquí hay una solución para aquellos de nosotros que nos sentimos incómodos al usar soluciones que dependen de las unittest
internas de la unidad:
Primero, creamos un decorador que establecerá un indicador en la instancia de TestCase
para determinar si el caso de prueba falló o pasó:
import unittest
import functools
def _tag_error(func):
"""Decorates a unittest test function to add failure information to the TestCase."""
@functools.wraps(func)
def decorator(self, *args, **kwargs):
"""Add failure information to `self` when `func` raises an exception."""
self.test_failed = False
try:
func(self, *args, **kwargs)
except unittest.SkipTest:
raise
except Exception: # pylint: disable=broad-except
self.test_failed = True
raise # re-raise the error with the original traceback.
return decorator
Este decorador es bastante simple. Se basa en el hecho de que unittest
detecta las pruebas fallidas a través de Excepciones . Hasta donde yo sé, la única excepción especial que necesita ser manejada es unittest.SkipTest
(que no indica una falla de prueba). Todas las demás excepciones indican fallas de prueba, por lo que las marcamos como tales cuando aparecen en burbujas para nosotros.
Ahora podemos usar este decorador directamente:
class MyTest(unittest.TestCase):
test_failed = False
def tearDown(self):
super(MyTest, self).tearDown()
print(self.test_failed)
@_tag_error
def test_something(self):
self.fail(''Bummer'')
Va a ser realmente molesto escribir este decorador todo el tiempo. ¿Hay alguna manera de simplificar? ¡Sí hay! * Podemos escribir una metaclase para manejar la aplicación del decorador para nosotros:
class _TestFailedMeta(type):
"""Metaclass to decorate test methods to append error information to the TestCase instance."""
def __new__(cls, name, bases, dct):
for name, prop in dct.items():
# assume that TestLoader.testMethodPrefix hasn''t been messed with -- otherwise, we''re hosed.
if name.startswith(''test'') and callable(prop):
dct[name] = _tag_error(prop)
return super(_TestFailedMeta, cls).__new__(cls, name, bases, dct)
Ahora aplicamos esto a nuestra subclase base TestCase
y estamos listos:
import six # For python2.x/3.x compatibility
class BaseTestCase(six.with_metaclass(_TestFailedMeta, unittest.TestCase)):
"""Base class for all our other tests.
We don''t really need this, but it demonstrates that the
metaclass gets applied to all subclasses too.
"""
class MyTest(BaseTestCase):
def tearDown(self):
super(MyTest, self).tearDown()
print(self.test_failed)
def test_something(self):
self.fail(''Bummer'')
Es probable que haya una cantidad de casos que esto no maneja adecuadamente. Por ejemplo, no detecta correctamente las subtests fallidas o las fallas esperadas. Me interesarían otros modos de falla de esto, así que si encuentras un caso que no estoy manejando correctamente, házmelo saber en los comentarios y lo investigaré.
* Si no hubiera una manera más fácil, no habría hecho _tag_error
una función privada ;-)
CAVEAT: No tengo forma de verificar la siguiente teoría en este momento, alejándome de una caja dev. Así que esto puede ser un tiro en la oscuridad.
Quizás pueda verificar el valor de retorno de sys.exc_info()
dentro de su método tearDown (), si devuelve (None, None, None)
, sabe que el caso de prueba fue exitoso. De lo contrario, podría usar la tupla devuelta para interrogar al objeto de excepción.
Consulte la documentación de sys.exc_info .
Otro enfoque más explícito es escribir un decorador de métodos que pueda aplicar a todos sus métodos de casos de prueba que requieren este manejo especial. Este decorador puede interceptar excepciones de afirmación y en función de eso modificar algún estado en self
permite que su método tearDown aprenda qué sucede.
@assertion_tracker
def test_foo(self):
# some test logic
Depende del tipo de informe que le gustaría producir.
En caso de que desee realizar algunas acciones en caso de error (como la generación de capturas de pantalla ), en lugar de utilizar tearDown()
, puede lograrlo anulando failureException
.
Por ejemplo:
@property
def failureException(self):
class MyFailureException(AssertionError):
def __init__(self_, *args, **kwargs):
screenshot_dir = ''reports/screenshots''
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
self.driver.save_screenshot(''{0}/{1}.png''.format(screenshot_dir, self.id()))
return super(MyFailureException, self_).__init__(*args, **kwargs)
MyFailureException.__name__ = AssertionError.__name__
return MyFailureException
El nombre de la prueba actual se puede recuperar con el método unittest.TestCase.id() . Entonces en tearDown puedes verificar self.id ().
El ejemplo muestra cómo:
- encontrar si la prueba actual tiene error o falla en la lista de errores o fallas
- imprimir identificación de prueba con PASS o FAIL o EXCEPTION
El ejemplo probado aquí funciona con el buen ejemplo de @scoffey.
def tearDown(self):
result = "PASS"
#### find and show result for current test
# I did not find any nicer/neater way of comparing self.id() with test id stored in errors or failures lists :-7
id = str(self.id()).split(''.'')[-1]
# id() e.g. tup[0]:<__main__.MyTest testMethod=test_onePlusNoneIsNone>
# str(tup[0]):"test_onePlusOneEqualsThree (__main__.MyTest)"
# str(self.id()) = __main__.MyTest.test_onePlusNoneIsNone
for tup in self.currentResult.failures:
if str(tup[0]).startswith(id):
print '' test %s failure:%s'' % (self.id(), tup[1])
## DO TEST FAIL ACTION HERE
result = "FAIL"
for tup in self.currentResult.errors:
if str(tup[0]).startswith(id):
print '' test %s error:%s'' % (self.id(), tup[1])
## DO TEST EXCEPTION ACTION HERE
result = "EXCEPTION"
print "Test:%s Result:%s" % (self.id(), result)
ejemplo de resultado:
python run_scripts/tut2.py 2>&1
E test __main__.MyTest.test_onePlusNoneIsNone error:Traceback (most recent call last):
File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: ''int'' and ''NoneType''
Test:__main__.MyTest.test_onePlusNoneIsNone Result:EXCEPTION
F test __main__.MyTest.test_onePlusOneEqualsThree failure:Traceback (most recent call last):
File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true
Test:__main__.MyTest.test_onePlusOneEqualsThree Result:FAIL
Test:__main__.MyTest.test_onePlusOneEqualsTwo Result:PASS
.
======================================================================
ERROR: test_onePlusNoneIsNone (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "run_scripts/tut2.py", line 80, in test_onePlusNoneIsNone
self.assertTrue(1 + None is None) # raises TypeError
TypeError: unsupported operand type(s) for +: ''int'' and ''NoneType''
======================================================================
FAIL: test_onePlusOneEqualsThree (__main__.MyTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "run_scripts/tut2.py", line 77, in test_onePlusOneEqualsThree
self.assertTrue(1 + 1 == 3) # fails
AssertionError: False is not true
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1, errors=1)
Esta solución es para las versiones de Python 2.7 a 3.6 (la versión actual más alta y el desarrollo poco antes de 3.7-alpha ), sin decoradores u otras modificaciones en ningún código antes de tearDown
. Todo funciona según la clasificación de resultados incorporada. También las pruebas omitidas o las fallas expectedFailure
se reconocen correctamente. Evalúa el resultado de la prueba actual, no un resumen de todas las pruebas aprobadas hasta el momento. Compatible también con pytest .
import unittest
class MyTest(unittest.TestCase):
def tearDown(self):
if hasattr(self, ''_outcome''): # Python 3.4+
result = self.defaultTestResult() # these 2 methods have no side effects
self._feedErrorsToResult(result, self._outcome.errors)
else: # Python 3.2 - 3.3 or 3.0 - 3.1 and 2.7
result = getattr(self, ''_outcomeForDoCleanups'', self._resultForDoCleanups)
error = self.list2reason(result.errors)
failure = self.list2reason(result.failures)
ok = not error and not failure
# demo: report short info immediately (not important)
if not ok:
typ, text = (''ERROR'', error) if error else (''FAIL'', failure)
msg = [x for x in text.split(''/n'')[1:] if not x.startswith('' '')][0]
print("/n%s: %s/n %s" % (typ, self.id(), msg))
def list2reason(self, exc_list):
if exc_list and exc_list[-1][0] is self:
return exc_list[-1][1]
# DEMO tests
def test_success(self):
self.assertEqual(1, 1)
def test_fail(self):
self.assertEqual(2, 1)
def test_error(self):
self.assertEqual(1 / 0, 1)
Comentarios: solo se deben informar una o cero excepciones (error o falla) porque no se puede esperar más antes de tearDown
. El paquete unittest
espera que tearDown pueda generar una segunda excepción. Por lo tanto, los errors
y failures
las listas pueden contener solo uno o cero elementos juntos antes de tearDown. Las líneas después del comentario "demo" informan un resultado breve.
Salida de demostración: (no importante)
$ python3.5 -m unittest test
EF.
ERROR: test.MyTest.test_error
ZeroDivisionError: division by zero
FAIL: test.MyTest.test_fail
AssertionError: 2 != 1
==========================================================
... skipped usual output from unittest with tracebacks ...
...
Ran 3 tests in 0.002s
FAILED (failures=1, errors=1)
Comparación con otras soluciones (con respecto al historial de commit del repositorio fuente de Python):
Esta solución usa un atributo privado de la instancia de TestCase como muchas otras soluciones, pero revisé cuidadosamente todas las confirmaciones relevantes en el repositorio de fuentes de Python de que tres nombres alternativos cubren el historial de códigos desde Python 2.7 a 3.6.2 sin ningún espacio. Puede ser un problema después de una nueva versión importante de Python, pero podría ser claramente reconocida, omitida y fácil de arreglar más adelante para una nueva versión de Python. Una ventaja es que no se modifica nada antes de ejecutar tearDown, nunca debe romperse la prueba y se admite toda la funcionalidad de unittest, funciona con pytest, podría funcionar con muchos paquetes extendidos, pero no con nosetest (no es una sorpresa porque nosetest no es compatible, por ejemplo, unittest.expectedFailure).
Las soluciones con decoradores en los métodos de prueba del usuario o con una mgilson personalizada ( mgilson , Pavel Repin 2nd way, kenorb) son robustas frente a futuras versiones de Python, pero si todo funcionara por completo, crecerían como una bola de nieve con excepciones más compatibles y más partes internas replicadas de unittest. Las funciones decoradas tienen trazas menos legibles (incluso más niveles agregados por un decorador), son más complicadas para la depuración y es desagradable si otro decorador más importante tiene un problema. (Gracias a mgilson, la funcionalidad básica está lista y los problemas conocidos pueden solucionarse).
La solución con método de
run
modificada y parámetro deresult
atrapado- ( scoffey ) debería funcionar también para Python 2.6. La interpretación de los resultados se puede mejorar a los requisitos de la pregunta, pero nada puede funcionar en Python 3.4+, porque el
result
se actualiza después de la llamada de tearDown, nunca antes. - Mark G .: (probado con Python 2.7, 3.2, 3.3, 3.4 y con nosetest)
- ( scoffey ) debería funcionar también para Python 2.6. La interpretación de los resultados se puede mejorar a los requisitos de la pregunta, pero nada puede funcionar en Python 3.4+, porque el
La solución de
exc_info()
(Pavel Repin 2º modo) funciona solo con Python 2.Otras soluciones son principalmente similares, pero menos completas o con más desventajas.
Explicado por el repositorio fuente de Python
= Lib / unittest / case.py =
Python v 2.7 - 3.3
class TestCase(object):
...
def run(self, result=None):
...
self._outcomeForDoCleanups = result # Python 3.2, 3.3
# self._resultForDoCleanups = result # Python 2.7
# # Python 2.6 - no result saved
...
try:
testMethod()
except... # many times for different exception classes
result.add...(self, sys.exc_info()) # _addSkip, addError, addFailure
...
try:
self.tearDown()
...
Python v. 3.4 - 3.6
def run(self, result=None):
...
# outocome is a context manager to catch and collect different exceptions
self._outcome = outcome
...
with outcome...(self):
testMethod()
...
with outcome...(self):
self.tearDown()
...
self._feedErrorsToResult(result, outcome.errors)
Nota (leyendo los mensajes de confirmación): una razón por la cual los resultados de las pruebas están tan desacoplados de las pruebas es la prevención de fugas de memoria . Toda información de excepción puede acceder a los marcos del estado de proceso fallido, incluidas todas las variables locales. Si se asigna un cuadro a una variable local en un bloque de código que también podría fallar, se podría crear fácilmente una referencia de memoria cruzada. No es terrible, gracias al recolector de basura, pero la memoria libre puede fragmentarse más rápidamente que si la memoria se liberara correctamente. Esta es una razón por la que la información de excepción y el rastreo se convierten muy pronto en cadenas y por qué los objetos temporales como self._outcome
están encapsulados y se configuran en Ninguno en un bloque finally
para evitar fugas de memoria.
Inspirado por la respuesta de scoffey , decidí llevar a Mercilessnes al siguiente nivel, y he descubierto lo siguiente.
Funciona tanto en vainilla unittest, y también cuando se ejecuta a través de nosetests, y también funciona en las versiones de Python 2.7, 3.2, 3.3 y 3.4 (no probé específicamente 3.0, 3.1 o 3.5, ya que no tengo estos instalados en el momento, pero si leo correctamente el código fuente , debería funcionar en 3.5 también):
#! /usr/bin/env python
from __future__ import unicode_literals
import logging
import os
import sys
import unittest
# Log file to see squawks during testing
formatter = logging.Formatter(fmt=''%(levelname)-8s %(name)s: %(message)s'')
log_file = os.path.splitext(os.path.abspath(__file__))[0] + ''.log''
handler = logging.FileHandler(log_file)
handler.setFormatter(formatter)
logging.root.addHandler(handler)
logging.root.setLevel(logging.DEBUG)
log = logging.getLogger(__name__)
PY = tuple(sys.version_info)[:3]
class SmartTestCase(unittest.TestCase):
"""Knows its state (pass/fail/error) by the time its tearDown is called."""
def run(self, result):
# Store the result on the class so tearDown can behave appropriately
self.result = result.result if hasattr(result, ''result'') else result
if PY >= (3, 4, 0):
self._feedErrorsToResultEarly = self._feedErrorsToResult
self._feedErrorsToResult = lambda *args, **kwargs: None # no-op
super(SmartTestCase, self).run(result)
@property
def errored(self):
if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.errors)
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.failures)
return self.id() in [case.id() for case, _ in self.result.failures]
@property
def passed(self):
return not (self.errored or self.failed)
def tearDown(self):
if PY >= (3, 4, 0):
self._feedErrorsToResultEarly(self.result, self._outcome.errors)
class TestClass(SmartTestCase):
def test_1(self):
self.assertTrue(True)
def test_2(self):
self.assertFalse(True)
def test_3(self):
self.assertFalse(False)
def test_4(self):
self.assertTrue(False)
def test_5(self):
self.assertHerp(''Derp'')
def tearDown(self):
super(TestClass, self).tearDown()
log.critical(''---- RUNNING {} ... -----''.format(self.id()))
if self.errored:
log.critical(''----- ERRORED -----'')
elif self.failed:
log.critical(''----- FAILED -----'')
else:
log.critical(''----- PASSED -----'')
if __name__ == ''__main__'':
unittest.main()
Cuando se ejecuta con unittest
:
$ ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]
$ cat ./test.log
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----
Cuando se ejecuta con nosetests
:
$ nosetests ./test.py -v
test_1 (test.TestClass) ... ok
test_2 (test.TestClass) ... FAIL
test_3 (test.TestClass) ... ok
test_4 (test.TestClass) ... FAIL
test_5 (test.TestClass) ... ERROR
$ cat ./test.log
CRITICAL test: ---- RUNNING test.TestClass.test_1 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_2 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_3 ... -----
CRITICAL test: ----- PASSED -----
CRITICAL test: ---- RUNNING test.TestClass.test_4 ... -----
CRITICAL test: ----- FAILED -----
CRITICAL test: ---- RUNNING test.TestClass.test_5 ... -----
CRITICAL test: ----- ERRORED -----
Fondo
Empecé con esto:
class SmartTestCase(unittest.TestCase):
"""Knows its state (pass/fail/error) by the time its tearDown is called."""
def run(self, result):
# Store the result on the class so tearDown can behave appropriately
self.result = result.result if hasattr(result, ''result'') else result
super(SmartTestCase, self).run(result)
@property
def errored(self):
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
return self.id() in [case.id() for case, _ in self.result.failures]
@property
def passed(self):
return not (self.errored or self.failed)
Sin embargo, esto solo funciona en Python 2. En Python 3, hasta e incluyendo 3.3, el flujo de control parece haber cambiado un poco: el paquete de test de Python 3 procesa los resultados después de llamar al método tearDown()
cada prueba ... este comportamiento puede confirmarse si simplemente agregamos una línea adicional (o seis) a nuestra clase de prueba:
@@ -63,6 +63,12 @@
log.critical(''----- FAILED -----'')
else:
log.critical(''----- PASSED -----'')
+ log.warning(
+ ''ERRORS THUS FAR:/n''
+ + ''/n''.join(tc.id() for tc, _ in self.result.errors))
+ log.warning(
+ ''FAILURES THUS FAR:/n''
+ + ''/n''.join(tc.id() for tc, _ in self.result.failures))
if __name__ == ''__main__'':
Luego simplemente vuelva a ejecutar las pruebas:
$ python3.3 ./test.py -v
test_1 (__main__.TestClass) ... ok
test_2 (__main__.TestClass) ... FAIL
test_3 (__main__.TestClass) ... ok
test_4 (__main__.TestClass) ... FAIL
test_5 (__main__.TestClass) ... ERROR
[…]
... y verá que obtiene esto como resultado:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
Ahora, compare lo anterior con la salida de Python 2:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_1 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_2 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_3 ... -----
CRITICAL __main__: ----- PASSED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_4 ... -----
CRITICAL __main__: ----- FAILED -----
WARNING __main__: ERRORS THUS FAR:
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
CRITICAL __main__: ---- RUNNING __main__.TestClass.test_5 ... -----
CRITICAL __main__: ----- ERRORED -----
WARNING __main__: ERRORS THUS FAR:
__main__.TestClass.test_5
WARNING __main__: FAILURES THUS FAR:
__main__.TestClass.test_2
__main__.TestClass.test_4
Dado que Python 3 procesa errores / fallas después de que la prueba se derriba, no podemos deducir fácilmente el resultado de una prueba usando result.errors
o result.failures
en todos los casos. (Creo que probablemente tenga más sentido arquitectónico procesar los resultados de una prueba después de demolerlo, sin embargo, hace que el caso de uso perfectamente válido de seguir un procedimiento de fin de prueba diferente en función del estado de aprobación / falla de una prueba sea un poco más difícil de encontrar ...)
Por lo tanto, en lugar de confiar en el objeto de result
general, podemos hacer referencia a _outcomeForDoCleanups
como already se already mencionado, que contiene el objeto de resultado para la prueba en ejecución y tiene los atributos necesarios de errors
y failrues
, que podemos usar para inferir una prueba estado en el momento en que se tearDown()
:
@@ -3,6 +3,7 @@
from __future__ import unicode_literals
import logging
import os
+import sys
import unittest
@@ -16,6 +17,9 @@
log = logging.getLogger(__name__)
+PY = tuple(sys.version_info)[:3]
+
+
class SmartTestCase(unittest.TestCase):
"""Knows its state (pass/fail/error) by the time its tearDown is called."""
@@ -27,10 +31,14 @@
@property
def errored(self):
+ if PY >= (3, 0, 0):
+ return bool(self._outcomeForDoCleanups.errors)
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
+ if PY >= (3, 0, 0):
+ return bool(self._outcomeForDoCleanups.failures)
return self.id() in [case.id() for case, _ in self.result.failures]
@property
Esto agrega soporte para las primeras versiones de Python 3.
A partir de Python 3.4, sin embargo, esta variable miembro privada ya no existe , y en su lugar, se agregó un nuevo método (aunque también privado): _feedErrorsToResult
.
Esto significa que para las versiones 3.4 ( y posteriores ), si la necesidad es lo suficientemente grande, uno puede, de manera muy hackosa , abrirse camino para hacer que todo vuelva a funcionar como lo hizo en la versión 2 ...
@@ -27,17 +27,20 @@
def run(self, result):
# Store the result on the class so tearDown can behave appropriately
self.result = result.result if hasattr(result, ''result'') else result
+ if PY >= (3, 4, 0):
+ self._feedErrorsToResultEarly = self._feedErrorsToResult
+ self._feedErrorsToResult = lambda *args, **kwargs: None # no-op
super(SmartTestCase, self).run(result)
@property
def errored(self):
- if PY >= (3, 0, 0):
+ if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.errors)
return self.id() in [case.id() for case, _ in self.result.errors]
@property
def failed(self):
- if PY >= (3, 0, 0):
+ if (3, 0, 0) <= PY < (3, 4, 0):
return bool(self._outcomeForDoCleanups.failures)
return self.id() in [case.id() for case, _ in self.result.failures]
@@ -45,6 +48,10 @@
def passed(self):
return not (self.errored or self.failed)
+ def tearDown(self):
+ if PY >= (3, 4, 0):
+ self._feedErrorsToResultEarly(self.result, self._outcome.errors)
+
class TestClass(SmartTestCase):
@@ -64,6 +71,7 @@
self.assertHerp(''Derp'')
def tearDown(self):
+ super(TestClass, self).tearDown()
log.critical(''---- RUNNING {} ... -----''.format(self.id()))
if self.errored:
log.critical(''----- ERRORED -----'')
... siempre que, por supuesto, todos los consumidores de esta clase recuerden super(…, self).tearDown()
en sus respectivos métodos tearDown
...
Descargo de responsabilidad: puramente educativo, no intente esto en casa, etc. etc. etc. No estoy particularmente orgulloso de esta solución, pero parece funcionar bastante bien por el momento, y es lo mejor que pude hackear después jugueteando durante una hora o dos un sábado por la tarde ...
Python 2.7.
También puede obtener el resultado después de unittest.main ():
t = unittest.main(exit=False)
print t.result
o use suite:
suite.addTests(tests)
result = unittest.result.TestResult()
suite.run(result)
print result
Si echa un vistazo a la implementación de unittest.TestCase.run
, puede ver que todos los resultados de la prueba se recopilan en el objeto de resultado (por lo general, una instancia de unittest.TestResult
) pasada como argumento. No se deja ningún estado de resultado en el objeto unittest.TestCase
.
Por lo tanto, no hay mucho que pueda hacer en el método unittest.TestCase.tearDown
menos que rompa sin piedad el desacoplamiento elegante de los casos de prueba y los resultados de las pruebas con algo como esto:
import unittest
class MyTest(unittest.TestCase):
currentResult = None # holds last result object passed to run method
def setUp(self):
pass
def tearDown(self):
ok = self.currentResult.wasSuccessful()
errors = self.currentResult.errors
failures = self.currentResult.failures
print '' All tests passed so far!'' if ok else /
'' %d errors and %d failures so far'' % /
(len(errors), len(failures))
def run(self, result=None):
self.currentResult = result # remember result for use in tearDown
unittest.TestCase.run(self, result) # call superclass run method
def test_onePlusOneEqualsTwo(self):
self.assertTrue(1 + 1 == 2) # succeeds
def test_onePlusOneEqualsThree(self):
self.assertTrue(1 + 1 == 3) # fails
def test_onePlusNoneIsNone(self):
self.assertTrue(1 + None is None) # raises TypeError
if __name__ == ''__main__'':
unittest.main()
EDITAR: Esto funciona para Python 2.6 - 3.3, (modificado para el nuevo Python bellow ).
Si está utilizando Python2, puede usar el método _resultForDoCleanups
. Este método devuelve un objeto TextTestResult
:
<unittest.runner.TextTestResult run=1 errors=0 failures=0>
Puede usar este objeto para verificar el resultado de sus pruebas:
def tearDown(self):
if self._resultForDoCleanups.failures:
...
elif self._resultForDoCleanups.errors:
...
else:
#Success
Si está utilizando Python3, puede usar _outcomeForDoCleanups
:
def tearDown(self):
if not self._outcomeForDoCleanups.success:
...
Siguiendo con la respuesta de amatellanes, si estás en Python3.4, no puedes usar _outcomeForDoCleanups
. Esto es lo que logré hackear juntos:
def _test_has_failed(self):
for method, error in self._outcome.errors:
if error:
return True
return False
asqueroso, pero parece funcionar.