Está utilizando pytest
, lo que le brinda amplias opciones para interactuar con las pruebas que fallan. Le ofrece opciones de línea de comandos y varios ganchos para que esto sea posible. Explicaré cómo usar cada uno y dónde puede hacer personalizaciones para satisfacer sus necesidades específicas de depuración.
También entraré en opciones más exóticas que le permitirán omitir afirmaciones específicas por completo, si realmente siente que debe hacerlo.
Manejar excepciones, no afirmar
Tenga en cuenta que una prueba fallida normalmente no detiene pytest; solo si habilitó explícitamente decirle que salga después de un cierto número de fallas . Además, las pruebas fallan porque se genera una excepción; assert
aumenta, AssertionError
pero esa no es la única excepción que hará que una prueba falle. Desea controlar cómo se manejan las excepciones, no modificarlas assert
.
Sin embargo, una aserción no va a finalizar la prueba individual. Esto se debe a que una vez que se genera una excepción fuera de un try...except
bloque, Python desenrolla el marco de la función actual y no hay retroceso en eso.
No creo que eso sea lo que quiere, a juzgar por su descripción de sus _assertCustom()
intentos de volver a ejecutar la afirmación, pero no obstante, analizaré sus opciones más adelante.
Depuración post-mortem en pytest con pdb
Para las diversas opciones para manejar fallas en un depurador, comenzaré con el --pdb
modificador de línea de comandos , que abre el indicador de depuración estándar cuando falla una prueba (salida por brevedad):
$ mkdir demo
$ touch demo/__init__.py
$ cat << EOF > demo/test_foo.py
> def test_ham():
> assert 42 == 17
> def test_spam():
> int("Vikings")
> EOF
$ pytest demo/test_foo.py --pdb
[ ... ]
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(2)test_ham()
-> assert 42 == 17
(Pdb) q
Exit: Quitting debugger
[ ... ]
Con este modificador, cuando una prueba falla, pytest inicia una sesión de depuración post mortem . Esto es esencialmente exactamente lo que querías; para detener el código en el punto de una prueba fallida y abrir el depurador para ver el estado de su prueba. Puede interactuar con las variables locales de la prueba, los globales y los locales y globales de cada cuadro en la pila.
Aquí pytest le da control total sobre si debe salir o no después de este punto: si usa el q
comando de salida, pytest también sale de la ejecución, el uso c
de continuar devolverá el control a pytest y se ejecutará la siguiente prueba.
Usar un depurador alternativo
No está obligado al pdb
depurador por esto; Puede configurar un depurador diferente con el --pdbcls
interruptor. Cualquier implementación pdb.Pdb()
compatible funcionaría, incluida la implementación del depurador de IPython , o la mayoría de los otros depuradores de Python (el depurador de pudb requiere que -s
se use el conmutador o un complemento especial ). El conmutador toma un módulo y una clase, por ejemplo, para usar pudb
podría usar:
$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger
Puede usar esta función para escribir su propia clase de envoltura Pdb
que simplemente regrese de inmediato si la falla específica no es algo que le interese. pytest
Utiliza Pdb()
exactamente lo mismo que pdb.post_mortem()
hace :
p = Pdb()
p.reset()
p.interaction(None, t)
Aquí, t
hay un objeto de rastreo . Cuando p.interaction(None, t)
regresa, pytest
continúa con la siguiente prueba, a menos que p.quitting
se establezca en True
(en ese momento, se cierra pytest).
Aquí hay un ejemplo de implementación que muestra que estamos rechazando la depuración y regresa de inmediato, a menos que se levante la prueba ValueError
, guardada como demo/custom_pdb.py
:
import pdb, sys
class CustomPdb(pdb.Pdb):
def interaction(self, frame, traceback):
if sys.last_type is not None and not issubclass(sys.last_type, ValueError):
print("Sorry, not interested in this failure")
return
return super().interaction(frame, traceback)
Cuando uso esto con la demostración anterior, esta es la salida (nuevamente, elegida por brevedad):
$ pytest test_foo.py -s --pdb --pdbcls=demo.custom_pdb:CustomPdb
[ ... ]
def test_ham():
> assert 42 == 17
E assert 42 == 17
test_foo.py:2: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Sorry, not interested in this failure
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Las introspectivas anteriores sys.last_type
para determinar si la falla es 'interesante'.
Sin embargo, realmente no puedo recomendar esta opción a menos que desee escribir su propio depurador usando tkInter o algo similar. Tenga en cuenta que es una gran empresa.
Fallas de filtrado; elegir y elegir cuándo abrir el depurador
El siguiente nivel es la depuración de pytest y los ganchos de interacción ; Estos son puntos de enlace para las personalizaciones de comportamiento, para reemplazar o mejorar la forma en que pytest normalmente maneja cosas como manejar una excepción o ingresar al depurador a través de pdb.set_trace()
o breakpoint()
(Python 3.7 o más reciente).
La implementación interna de este enlace también es responsable de imprimir el >>> entering PDB >>>
banner anterior, por lo que usar este enlace para evitar que se ejecute el depurador significa que no verá esta salida en absoluto. Puede tener su propio gancho y luego delegarlo al gancho original cuando un fallo de prueba es 'interesante', y así filtrar los fallos de prueba independientemente del depurador que esté utilizando. Puede acceder a la implementación interna accediendo por nombre ; el complemento de enlace interno para esto se llama pdbinvoke
. Para evitar que se ejecute, debe anular el registro pero guardar una referencia, ¿podemos llamarlo directamente según sea necesario?
Aquí hay una implementación de muestra de tal gancho; puede poner esto en cualquiera de las ubicaciones desde las que se cargan los complementos ; Lo puse en demo/conftest.py
:
import pytest
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
# unregister returns the unregistered plugin
pdbinvoke = config.pluginmanager.unregister(name="pdbinvoke")
if pdbinvoke is None:
# no --pdb switch used, no debugging requested
return
# get the terminalreporter too, to write to the console
tr = config.pluginmanager.getplugin("terminalreporter")
# create or own plugin
plugin = ExceptionFilter(pdbinvoke, tr)
# register our plugin, pytest will then start calling our plugin hooks
config.pluginmanager.register(plugin, "exception_filter")
class ExceptionFilter:
def __init__(self, pdbinvoke, terminalreporter):
# provide the same functionality as pdbinvoke
self.pytest_internalerror = pdbinvoke.pytest_internalerror
self.orig_exception_interact = pdbinvoke.pytest_exception_interact
self.tr = terminalreporter
def pytest_exception_interact(self, node, call, report):
if not call.excinfo. errisinstance(ValueError):
self.tr.write_line("Sorry, not interested!")
return
return self.orig_exception_interact(node, call, report)
El complemento anterior usa el TerminalReporter
complemento interno para escribir líneas en el terminal; esto hace que la salida sea más limpia cuando se usa el formato de estado de prueba compacto predeterminado y le permite escribir cosas en el terminal incluso con la captura de salida habilitada.
El ejemplo registra el objeto del complemento con pytest_exception_interact
gancho a través de otro gancho, pytest_configure()
pero asegurándose de que se ejecute lo suficientemente tarde (usando @pytest.hookimpl(trylast=True)
) para poder anular el registro del pdbinvoke
complemento interno . Cuando se llama al gancho, el ejemplo prueba contra el call.exceptinfo
objeto ; También puede comprobar el nodo o el informe también.
Con el código de ejemplo anterior en su lugar demo/conftest.py
, test_ham
se ignora el test_spam
fallo de la prueba , solo el fallo de la prueba, que aumenta ValueError
, da como resultado la apertura del mensaje de depuración:
$ pytest demo/test_foo.py --pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
-> int("Vikings")
(Pdb)
Para repetir, el enfoque anterior tiene la ventaja adicional de que puede combinar esto con cualquier depurador que funcione con pytest , incluido pudb, o el depurador de IPython:
$ pytest demo/test_foo.py --pdb --pdbcls=IPython.core.debugger:Pdb
[ ... ]
demo/test_foo.py F
Sorry, not interested!
demo/test_foo.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /.../demo/test_foo.py(4)test_spam()
1 def test_ham():
2 assert 42 == 17
3 def test_spam():
----> 4 int("Vikings")
ipdb>
También tiene mucho más contexto sobre qué prueba se estaba ejecutando (a través del node
argumento) y acceso directo a la excepción planteada (a través de la call.excinfo
ExceptionInfo
instancia).
Tenga en cuenta que los complementos específicos del depurador pytest (como pytest-pudb
o pytest-pycharm
) registran su propio pytest_exception_interact
hooksp. Una implementación más completa tendría que recorrer todos los complementos en el administrador de complementos para anular complementos arbitrarios, automáticamente, usando config.pluginmanager.list_name_plugin
y hasattr()
para probar cada complemento.
Hacer que los fracasos desaparezcan por completo
Si bien esto le da control total sobre la depuración de prueba fallida, esto deja la prueba como fallida incluso si optó por no abrir el depurador para una prueba determinada. Si desea hacer fallos desaparecen por completo, se puede hacer uso de un gancho diferente: pytest_runtest_call()
.
Cuando pytest ejecuta pruebas, ejecutará la prueba a través del enlace anterior, que se espera que regrese None
o genere una excepción. A partir de esto, se crea un informe, opcionalmente se crea una entrada de registro, y si la prueba falla, pytest_exception_interact()
se llama al enlace mencionado anteriormente . Entonces, todo lo que necesita hacer es cambiar el resultado que produce este gancho; en lugar de una excepción, simplemente no debería devolver nada en absoluto.
La mejor manera de hacerlo es usar una envoltura de gancho . Los envoltorios de ganchos no tienen que hacer el trabajo real, sino que tienen la oportunidad de alterar lo que sucede con el resultado de un gancho. Todo lo que tienes que hacer es agregar la línea:
outcome = yield
en su gancho envoltorio aplicación y se obtiene acceso al resultado de gancho , incluyendo la excepción de prueba a través outcome.excinfo
. Este atributo se establece en una tupla de (tipo, instancia, rastreo) si se produjo una excepción en la prueba. Alternativamente, puede llamar outcome.get_result()
y usar el try...except
manejo estándar .
Entonces, ¿cómo se pasa una prueba fallida? Tienes 3 opciones básicas:
- Puede marcar la prueba como una falla esperada , llamando
pytest.xfail()
al contenedor.
- Puede marcar el elemento como omitido , lo que pretende que la prueba nunca se ejecutó en primer lugar, llamando
pytest.skip()
.
- Puede eliminar la excepción, utilizando el
outcome.force_result()
método ; establezca el resultado en una lista vacía aquí (es decir: el enlace registrado no produjo nada más que None
), y la excepción se borra por completo.
Lo que uses depende de ti. Asegúrese de verificar primero el resultado de las pruebas omitidas y de falla esperada, ya que no necesita manejar esos casos como si la prueba fallara. Puede acceder a las excepciones especiales que generan estas opciones mediante pytest.skip.Exception
y pytest.xfail.Exception
.
Aquí hay un ejemplo de implementación que marca las pruebas fallidas que no aumentan ValueError
, como omitidas :
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
outcome = yield
try:
outcome.get_result()
except (pytest.xfail.Exception, pytest.skip.Exception, pytest.exit.Exception):
raise # already xfailed, skipped or explicit exit
except ValueError:
raise # not ignoring
except (pytest.fail.Exception, Exception):
# turn everything else into a skip
pytest.skip("[NOTRUN] ignoring everything but ValueError")
Cuando se pone en conftest.py
la salida se convierte en:
$ pytest -r a demo/test_foo.py
============================= test session starts =============================
platform darwin -- Python 3.8.0, pytest-3.10.0, py-1.7.0, pluggy-0.8.0
rootdir: ..., inifile:
collected 2 items
demo/test_foo.py sF [100%]
=================================== FAILURES ===================================
__________________________________ test_spam ___________________________________
def test_spam():
> int("Vikings")
E ValueError: invalid literal for int() with base 10: 'Vikings'
demo/test_foo.py:4: ValueError
=========================== short test summary info ============================
FAIL demo/test_foo.py::test_spam
SKIP [1] .../demo/conftest.py:12: [NOTRUN] ignoring everything but ValueError
===================== 1 failed, 1 skipped in 0.07 seconds ======================
Usé la -r a
bandera para aclarar que test_ham
se omitió ahora.
Si reemplaza la pytest.skip()
llamada con pytest.xfail("[XFAIL] ignoring everything but ValueError")
, la prueba se marca como una falla esperada:
[ ... ]
XFAIL demo/test_foo.py::test_ham
reason: [XFAIL] ignoring everything but ValueError
[ ... ]
y usando outcome.force_result([])
marcas lo aprobó:
$ pytest -v demo/test_foo.py # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED [ 50%]
Depende de usted cuál cree que se adapta mejor a su caso de uso. For skip()
y xfail()
yo imité el formato de mensaje estándar (con el prefijo [NOTRUN]
o [XFAIL]
), pero puede usar cualquier otro formato de mensaje que desee.
En los tres casos, pytest no abrirá el depurador para pruebas cuyo resultado haya alterado utilizando este método.
Alteración de declaraciones de afirmación individuales
Si desea modificar las assert
pruebas dentro de una prueba , entonces se está preparando para mucho más trabajo. Sí, esto es técnicamente posible, pero solo reescribiendo el código que Python ejecutará en el momento de la compilación .
Cuando lo usas pytest
, esto ya se está haciendo . Pytest reescribe assert
declaraciones para darle más contexto cuando sus afirmaciones fallan ; vea esta publicación de blog para obtener una buena descripción de lo que se está haciendo exactamente, así como el _pytest/assertion/rewrite.py
código fuente . Tenga en cuenta que ese módulo tiene más de 1k líneas de largo y requiere que comprenda cómo funcionan los árboles de sintaxis abstracta de Python . Si lo hace, podría monopatch ese módulo para agregar sus propias modificaciones allí, incluido el entorno assert
con un try...except AssertionError:
controlador.
Sin embargo , no puede simplemente deshabilitar o ignorar las afirmaciones de forma selectiva, porque las declaraciones posteriores podrían depender fácilmente del estado (arreglos de objetos específicos, conjunto de variables, etc.) contra el cual una afirmación omitida estaba destinada a proteger. Si una afirmación prueba que foo
no es así None
, entonces foo.bar
se espera que exista una afirmación posterior , entonces simplemente se encontrará con un AttributeError
allí, etc. Si desea seguir esta ruta, siga adelante con la excepción.
No voy a entrar en más detalles sobre la reescritura asserts
aquí, ya que no creo que valga la pena seguir esto, no dada la cantidad de trabajo involucrado, y con la depuración post mortem que le da acceso al estado de la prueba en el falla de punto de afirmación de todos modos .
Tenga en cuenta que si desea hacer esto, no necesita usar eval()
(lo que no funcionaría de todos modos, assert
es una declaración, por lo que necesitaría usar exec()
en su lugar), ni tendría que ejecutar la afirmación dos veces (que puede generar problemas si la expresión utilizada en el estado alterado de aserción). En su lugar, incrustará el ast.Assert
nodo dentro de un ast.Try
nodo y adjuntará un controlador excepto que use un ast.Raise
nodo vacío para volver a generar la excepción que se detectó.
Usar el depurador para omitir declaraciones de aserción.
El depurador de Python realmente le permite omitir declaraciones , utilizando el comando j
/jump
. Si usted sabe por adelantado que una afirmación específica será fallar, puede utilizar esta prescindir de ella. Puede ejecutar sus pruebas con --trace
, lo que abre el depurador al comienzo de cada prueba , luego emitir un j <line after assert>
para omitirlo cuando el depurador está en pausa justo antes de la afirmación.
Incluso puedes automatizar esto. Usando las técnicas anteriores, puede crear un complemento de depurador personalizado que
- usa el
pytest_testrun_call()
gancho para atrapar la AssertionError
excepción
- extrae el número de línea 'ofensiva' de la línea del rastreo, y quizás con algún análisis de código fuente determina los números de línea antes y después de la afirmación requerida para ejecutar un salto exitoso
- ejecuta la prueba nuevamente , pero esta vez utilizando una
Pdb
subclase que establece un punto de interrupción en la línea antes de la afirmación, y ejecuta automáticamente un salto al segundo cuando se alcanza el punto de interrupción, seguido de una c
continuación.
O, en lugar de esperar a que falle una aserción, puede automatizar el establecimiento de puntos de interrupción para cada uno assert
encontrado en una prueba (nuevamente utilizando el análisis de código fuente, puede extraer trivialmente números de línea para ast.Assert
nodos en un AST de la prueba), ejecute la prueba afirmada usando los comandos con guión del depurador y use el jump
comando para omitir la aserción misma. Tendrías que hacer una compensación; ejecute todas las pruebas bajo un depurador (que es lento ya que el intérprete tiene que llamar a una función de rastreo para cada instrucción) o solo aplique esto a las pruebas fallidas y pague el precio de volver a ejecutar esas pruebas desde cero.
Tal complemento sería mucho trabajo para crear, no voy a escribir un ejemplo aquí, en parte porque no encajaría en una respuesta de todos modos, y en parte porque no creo que valga la pena . Simplemente abría el depurador y hacía el salto manualmente. Una afirmación fallida indica un error en la prueba en sí o en el código bajo prueba, por lo que también puede centrarse en depurar el problema.