¿Es posible cambiar el comportamiento de la declaración de afirmación de PyTest en Python?

18

Estoy usando declaraciones de afirmación de Python para que coincida con el comportamiento real y esperado. No tengo control sobre estos, ya que si hay una prueba de error, aborta los casos. Quiero tomar el control del error de afirmación y quiero definir si quiero abortar el caso de prueba en caso de afirmación de falla o no.

También quiero agregar algo como si hay un error de aserción, entonces el caso de prueba debe pausarse y el usuario puede reanudar en cualquier momento.

No tengo idea de cómo hacer esto

Ejemplo de código, estamos usando pytest aquí

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Below is my expectation

Cuando afirmar arroja una afirmaciónError, debería tener la opción de pausar el caso de prueba y puedo depurar y luego reanudar. Para pausar y reanudar, usaré el tkintermódulo. Haré una función de aserción como a continuación

import tkinter
import tkinter.messagebox

top = tkinter.Tk()

def _assertCustom(assert_statement, pause_on_fail = 0):
    #assert_statement will be something like: assert a == 10, "Some error"
    #pause_on_fail will be derived from global file where I can change it on runtime
    if pause_on_fail == 1:
        try:
            eval(assert_statement)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            eval (assert_statement)
            #Above is to raise the assertion error again to fail the testcase
    else:
        eval (assert_statement)

En el futuro, tengo que cambiar cada declaración de aserción con esta función como

import pytest
def test_abc():
    a = 10
    # Suppose some code and below is the assert statement 
    _assertCustom("assert a == 10, 'error message'")

Este es un esfuerzo demasiado grande para mí, ya que tengo que hacer cambios en miles de lugares donde he usado la afirmación. ¿Hay alguna manera fácil de hacer eso enpytest

Summary:Necesito algo donde pueda pausar el caso de prueba en caso de falla y luego reanudarlo después de la depuración. Lo sé tkintery esa es la razón por la que lo he usado. Cualquier otra idea será bienvenida

Note: El código anterior aún no se ha probado. También puede haber pequeños errores de sintaxis

Editar: Gracias por las respuestas. Extendiendo esta pregunta un poco más adelante ahora. ¿Qué pasa si quiero cambiar el comportamiento de afirmar? Actualmente, cuando hay un error de aserción, sale el caso de prueba. ¿Qué pasa si quiero elegir si necesito salir de caso de prueba en caso de fallo de afirmación particular o no? No quiero escribir una función de aserción personalizada como se mencionó anteriormente porque de esta manera tengo que cambiar en varios lugares

Nitesh
fuente
3
¿Podría darnos un código de ejemplo de lo que le gustaría hacer?
mrblewog
1
No utilice, assertpero escriba sus propias funciones de verificación que hagan lo que quiera.
molbdnilo
¿Por qué no inserta afirmar en el bloque try y el mensaje de error excepto ?
Prathik Kini el
1
Parece que lo que realmente quieres es usar pytestpara tus casos de prueba. Es compatible con el uso de aserción y saltarse las pruebas junto con muchas más características que hacen que la escritura de pruebas más fácil.
blubberdiblub
1
¿No sería bastante sencillo escribir una herramienta simple que reemplazaría mecánicamente cada uno assert cond, "msg"en su código _assertCustom("assert cond, 'msg'")? Probablemente una sedfrase podría hacerlo.
NPE

Respuestas:

23

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; assertaumenta, AssertionErrorpero 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...exceptbloque, 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 --pdbmodificador 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 qcomando de salida, pytest también sale de la ejecución, el uso cde continuar devolverá el control a pytest y se ejecutará la siguiente prueba.

Usar un depurador alternativo

No está obligado al pdbdepurador por esto; Puede configurar un depurador diferente con el --pdbclsinterruptor. 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 -sse use el conmutador o un complemento especial ). El conmutador toma un módulo y una clase, por ejemplo, para usar pudbpodría usar:

$ pytest -s --pdb --pdbcls=pudb.debugger:Debugger

Puede usar esta función para escribir su propia clase de envoltura Pdbque simplemente regrese de inmediato si la falla específica no es algo que le interese. pytestUtiliza Pdb()exactamente lo mismo que pdb.post_mortem()hace :

p = Pdb()
p.reset()
p.interaction(None, t)

Aquí, thay un objeto de rastreo . Cuando p.interaction(None, t)regresa, pytestcontinú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_typepara 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 TerminalReportercomplemento 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_interactgancho 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 pdbinvokecomplemento interno . Cuando se llama al gancho, el ejemplo prueba contra el call.exceptinfoobjeto ; 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_hamse ignora el test_spamfallo 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 nodeargumento) y acceso directo a la excepción planteada (a través de la call.excinfo ExceptionInfoinstancia).

Tenga en cuenta que los complementos específicos del depurador pytest (como pytest-pudbo pytest-pycharm) registran su propio pytest_exception_interacthooksp. 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_pluginy 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 Noneo 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...exceptmanejo 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.Exceptiony 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.pyla 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 abandera para aclarar que test_hamse 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 assertpruebas 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 assertdeclaraciones 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.pycó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 assertcon 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 foono es así None, entonces foo.barse espera que exista una afirmación posterior , entonces simplemente se encontrará con un AttributeErrorallí, etc. Si desea seguir esta ruta, siga adelante con la excepción.

No voy a entrar en más detalles sobre la reescritura assertsaquí, 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, assertes 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.Assertnodo dentro de un ast.Trynodo y adjuntará un controlador excepto que use un ast.Raisenodo 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 AssertionErrorexcepció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 Pdbsubclase 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 ccontinuación.

O, en lugar de esperar a que falle una aserción, puede automatizar el establecimiento de puntos de interrupción para cada uno assertencontrado en una prueba (nuevamente utilizando el análisis de código fuente, puede extraer trivialmente números de línea para ast.Assertnodos en un AST de la prueba), ejecute la prueba afirmada usando los comandos con guión del depurador y use el jumpcomando 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.

Martijn Pieters
fuente
7

Puede lograr exactamente lo que desea sin absolutamente ninguna modificación de código con pytest --pdb .

Con tu ejemplo:

import pytest
def test_abc():
    a = 9
    assert a == 10, "some error message"

Ejecutar con --pdb:

py.test --pdb
collected 1 item

test_abc.py F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    def test_abc():
        a = 9
>       assert a == 10, "some error message"
E       AssertionError: some error message
E       assert 9 == 10

test_abc.py:4: AssertionError
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /private/tmp/a/test_abc.py(4)test_abc()
-> assert a == 10, "some error message"
(Pdb) p a
9
(Pdb)

Tan pronto como una prueba falla, puede depurarla con el depurador de python incorporado. Si ha terminado de depurar, puede hacerlo continuecon el resto de las pruebas.

gnvk
fuente
Esto se detendrá cuando el caso de prueba falle o el paso de prueba falle.
Nitesh
Consulte los documentos vinculados: doc.pytest.org/en/latest/…
gnvk
Excelente idea. Pero si usa --pdb, testcase se detendrá en cada falla. ¿Puedo decidir en tiempo de ejecución en qué falla quiero pausar el caso de prueba
Nitesh
5

Si está utilizando PyCharm, puede agregar un punto de interrupción de excepción para pausar la ejecución cada vez que falla una aserción. Seleccione Ver puntos de interrupción (CTRL-SHIFT-F8) y agregue un controlador de excepciones en aumento para AssertionError. Tenga en cuenta que esto puede ralentizar la ejecución de las pruebas.

De lo contrario, si no le importa detenerse al final de cada prueba fallida (justo antes de que se produzca un error) en lugar de hacerlo en el momento en que falla la afirmación, entonces tiene algunas opciones. Sin embargo, tenga en cuenta que en este punto ya podrían haberse ejecutado varios códigos de limpieza, como el cierre de archivos que se abrieron en la prueba. Las opciones posibles son:

  1. Puede decirle a pytest que lo coloque en el depurador de errores utilizando la opción --pdb .

  2. Puede definir el siguiente decorador y decorar cada función de prueba relevante con él. (Además de registrar un mensaje, también puede iniciar un pdb.post_mortem en este punto, o incluso un código interactivo. Interactuar con los locales del marco donde se originó la excepción, como se describe en esta respuesta ).

from functools import wraps

def pause_on_assert(test_func):
    @wraps(test_func)
    def test_wrapper(*args, **kwargs):
        try:
            test_func(*args, **kwargs)
        except AssertionError as e:
            tkinter.messagebox.showinfo(e)
            # re-raise exception to make the test fail
            raise
    return test_wrapper

@pause_on_assert
def test_abc()
    a = 10
    assert a == 2, "some error message"
  1. Si no desea decorar manualmente cada función de prueba, puede definir un dispositivo de uso automático que inspeccione sys.last_value :
import sys

@pytest.fixture(scope="function", autouse=True)
def pause_on_assert():
    yield
    if hasattr(sys, 'last_value') and isinstance(sys.last_value, AssertionError):
        tkinter.messagebox.showinfo(sys.last_value)
Uri Granta
fuente
Me gustó la respuesta con decoradores pero no se puede hacer dinámicamente. Quiero controlar dinámicamente cuándo quiero pause_on_assert o no. ¿Hay alguna solución para esto?
Nitesh
Dinámico de qué manera? ¿Como en un solo interruptor para habilitarlo / deshabilitarlo en todas partes? ¿O alguna forma de controlarlo para cada prueba?
Uri Granta
Supongamos que estoy ejecutando algunos casos de prueba. En el medio, tuve la necesidad de pausar en caso de fracaso. Activaré el interruptor. Más tarde, en cualquier momento, siento que necesito desactivar el interruptor.
Nitesh
Su decorador en respuesta: 2 no funcionará para mí porque mi caso de prueba tendrá múltiples afirmaciones
Nitesh
Con respecto a un 'interruptor', puede actualizar cualquiera de las implementaciones pause_on_assertpara leer desde un archivo para decidir si hacer una pausa o no.
Uri Granta
4

Una solución simple, si está dispuesto a usar Visual Studio Code, podría ser usar puntos de interrupción condicionales .

Esto le permitiría configurar sus afirmaciones, por ejemplo:

import pytest
def test_abc():
    a = 10
    assert a == 10, "some error message"

Luego agregue un punto de interrupción condicional en su línea de afirmación que solo se romperá cuando su afirmación falle:

ingrese la descripción de la imagen aquí

Nick Martin
fuente
@Nitesh: creo que esta solución resuelve todos sus problemas, solo se rompe cuando falla una aserción, puede depurar el código allí y luego y puede continuar con las pruebas restantes después ... Aunque es un poco más complicado de configurar inicialmente
Nick Martin