È possibile modificare il comportamento dell'istruzione assert PyTest in Python


18

Sto usando affermazioni Python asserire per abbinare il comportamento effettivo e previsto. Non ho il controllo su questi come se ci fosse un errore nei casi di prova si interrompe. Voglio prendere il controllo dell'errore di asserzione e voglio definire se desidero interrompere o meno la testcase in caso di insuccesso.

Inoltre voglio aggiungere qualcosa come se ci fosse un errore di asserzione, quindi il caso di test dovrebbe essere messo in pausa e l'utente può riprendere in qualsiasi momento.

Non ho idea di come farlo

Esempio di codice, stiamo usando pytest qui

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

Below is my expectation

Quando assert genera un'asserzioneError, dovrei avere la possibilità di mettere in pausa la testcase e posso eseguire il debug e riprendere successivamente. Per mettere in pausa e riprendere userò il tkintermodulo. Farò una funzione di asserzione come di seguito

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)

Andando avanti, devo cambiare ogni affermazione con questa funzione come

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

Questo è uno sforzo eccessivo per me in quanto devo apportare cambiamenti in migliaia di luoghi in cui ho usato assert. C'è un modo semplice per farlo inpytest

Summary:Ho bisogno di qualcosa in cui posso mettere in pausa il testcase in caso di errore e quindi riprendere dopo il debug. Lo so tkintere questo è il motivo per cui l'ho usato. Qualsiasi altra idea sarà benvenuta

Note: Il codice sopra non è ancora stato testato. Potrebbero esserci anche piccoli errori di sintassi

Modifica: grazie per le risposte. Estendere questa domanda un po 'avanti ora. E se volessi cambiare il comportamento di assert. Attualmente, quando si verifica un errore di asserzione, si esce da Testcase. E se volessi scegliere se ho bisogno di uscire dalla testcase in caso di asserzione particolare oppure no. Non voglio scrivere la funzione di asserzione personalizzata come menzionato sopra perché in questo modo devo cambiare in numero di posti


3
Potresti darci un esempio di codice di cosa ti piacerebbe fare?
mrblewog,

1
Non usare assertma scrivi le tue funzioni di controllo che fanno quello che vuoi.
molbdnilo,

Perché non inserire assert nel blocco try e il messaggio di errore in tranne ?
Prathik Kini,

1
Sembra che quello che vuoi davvero sia usare pytestper i tuoi casi di test. Supporta l'utilizzo di assert e il salto dei test insieme a molte altre funzionalità che facilitano la scrittura di test suite.
Blubberdiblub,

1
Non sarebbe abbastanza semplice scrivere un semplice strumento che sostituisca meccanicamente ogni assert cond, "msg"codice nel tuo codice _assertCustom("assert cond, 'msg'")? Probabilmente un sedone-liner potrebbe farlo.
NPE,

Risposte:


23

Stai utilizzando pytest, il che ti offre ampie opzioni per interagire con test falliti. Ti offre opzioni da linea di comando e diversi hook per renderlo possibile. Spiegherò come utilizzare ciascuno e dove è possibile effettuare personalizzazioni per soddisfare le esigenze di debug specifiche.

Vado anche in opzioni più esotiche che ti permetterebbero di saltare del tutto specifiche asserzioni, se davvero senti di doverlo fare.

Gestire le eccezioni, non affermare

Si noti che un test fallito normalmente non ferma il pytest; solo se hai abilitato a dire esplicitamente di uscire dopo un certo numero di errori . Inoltre, i test falliscono perché viene sollevata un'eccezione; assertsolleva AssertionErrorma questa non è l'unica eccezione che farà fallire un test! Vuoi controllare come vengono gestite le eccezioni, non modificarle assert.

Tuttavia, una mancanza di asserzione si terminare il singolo test. Questo perché una volta sollevata un'eccezione al di fuori di un try...exceptblocco, Python scioglie il frame della funzione corrente e non si può tornare indietro.

Non penso che sia quello che vuoi, a giudicare dalla descrizione dei tuoi _assertCustom()tentativi di ripetere l'asserzione, ma discuterò comunque le tue opzioni più in basso.

Debug post mortem in pytest con pdb

Per le varie opzioni per gestire gli errori in un debugger, inizierò con l'opzione della --pdbriga di comando , che apre il prompt di debug standard quando un test fallisce (output eluito per brevità):

$ 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 questa opzione, quando un test fallisce pytest avvia una sessione di debug post mortem . Questo è essenzialmente esattamente quello che volevi; per fermare il codice nel punto di un test fallito e aprire il debugger per dare un'occhiata allo stato del test. È possibile interagire con le variabili locali del test, i globali e i locali e i globali di ogni frame nello stack.

Qui pytest ti dà il pieno controllo sull'uscita o meno dopo questo punto: se usi il qcomando quit, anche pytest esce anche dalla corsa, usando cfor continue restituirà il controllo a pytest e il test successivo verrà eseguito.

Utilizzo di un debugger alternativo

Non sei vincolato al pdbdebugger per questo; è possibile impostare un debugger diverso con l' --pdbclsopzione Qualsiasi implementazione pdb.Pdb()compatibile funzionerebbe, inclusa l' implementazione del debugger IPython o la maggior parte degli altri debugger Python (il debugger pudb richiede l' -sutilizzo dello switch o un plug-in speciale ). Lo switch accetta un modulo e una classe, ad es. Per usarlo pudbpuoi usare:

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

È possibile utilizzare questa funzione per scrivere la propria classe wrapper Pdbche ritorni immediatamente immediatamente se l'errore specifico non è qualcosa a cui si è interessati. pytestUtilizza Pdb()esattamente come pdb.post_mortem()fa :

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

Qui, tè un oggetto traceback . Quando p.interaction(None, t)ritorna, pytestcontinua con il test successivo, a meno che non p.quitting sia impostato su True(a quel punto pytest quindi esce).

Ecco un'implementazione di esempio che stampa che stiamo rifiutando di eseguire il debug e restituisce immediatamente, a meno che il test non sia stato sollevato ValueError, salvato come 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)

Quando lo uso con la demo di cui sopra, viene emesso (di nuovo, eluito per brevità):

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

Gli introspetti di cui sopra sys.last_typeper determinare se l'errore è "interessante".

Tuttavia, non posso davvero raccomandare questa opzione a meno che tu non voglia scrivere il tuo debugger usando tkInter o qualcosa di simile. Si noti che questa è una grande impresa.

Errori di filtraggio; scegli e scegli quando aprire il debugger

Il livello successivo è il debugging pytest e gli hook di interazione ; questi sono punti di aggancio per le personalizzazioni del comportamento, per sostituire o migliorare il modo in cui pytest normalmente gestisce cose come la gestione di un'eccezione o l'inserimento del debugger tramite pdb.set_trace()o breakpoint()(Python 3.7 o versioni successive).

L'implementazione interna di questo hook è responsabile anche della stampa del >>> entering PDB >>>banner sopra, quindi l'utilizzo di questo hook per impedire l'esecuzione del debugger significa che non vedrai affatto questo output. Puoi avere il tuo hook personale quindi delegarlo al hook originale quando un errore di test è "interessante", e quindi i fallimenti del test di filtro sono indipendenti dal debugger che stai usando! Puoi accedere all'implementazione interna accedendo per nome ; il plugin hook interno per questo è chiamato pdbinvoke. Per impedirne l'esecuzione è necessario annullare la registrazione ma salvare un riferimento, possiamo chiamarlo direttamente secondo necessità.

Ecco una esempio di implementazione di un tale hook; puoi metterlo in una delle posizioni in cui sono caricati i plugin ; L'ho inserito 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)

Il plug-in sopra utilizza il TerminalReporterplug-in interno per scrivere le righe sul terminale; questo rende l'output più pulito quando si utilizza il formato di stato del test compatto predefinito e consente di scrivere elementi sul terminale anche con l'acquisizione dell'output abilitata.

L'esempio registra l'oggetto plugin con pytest_exception_interacthook tramite un altro hook, pytest_configure()ma assicurandosi che funzioni abbastanza tardi (usando @pytest.hookimpl(trylast=True)) per poter annullare la registrazione del pdbinvokeplugin interno . Quando viene chiamato l'hook, l'esempio verifica l' call.exceptinfooggetto ; puoi anche controllare il nodo o anche il rapporto .

Con il codice di esempio sopra inserito demo/conftest.py, l' test_hamerrore di test viene ignorato, solo l' test_spamerrore di test, che genera ValueError, provoca l'apertura del prompt di debug:

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

Per reiterare, l'approccio sopra ha l'ulteriore vantaggio di poterlo combinare con qualsiasi debugger che funziona con pytest , incluso pudb, o il debugger 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>

Ha anche molto più contesto su quale test è stato eseguito (tramite l' nodeargomento) e l'accesso diretto all'eccezione sollevata (tramite l' call.excinfo ExceptionInfoistanza).

Si noti che specifici plug-in di debugger pytest (come pytest-pudbo pytest-pycharm) registrano il proprio pytest_exception_interacthooksp. Un'implementazione più completa dovrebbe passare in rassegna tutti i plug-in nel gestore dei plug-in per sostituire automaticamente i plug-in arbitrari, utilizzando config.pluginmanager.list_name_plugine hasattr()testando ogni plug-in.

Far scomparire del tutto i fallimenti

Mentre questo ti dà il pieno controllo sul debug del test fallito, questo lascia comunque il test come fallito anche se hai scelto di non aprire il debugger per un dato test. Se si vuole fare errori andare via del tutto, si può fare uso di un gancio diverso: pytest_runtest_call().

Quando pytest esegue i test, eseguirà il test tramite l'hook sopra, che dovrebbe restituire Noneo sollevare un'eccezione. Da questo viene creato un rapporto, facoltativamente viene creata una voce di registro e se il test ha esito negativo, pytest_exception_interact()viene chiamato l' hook di cui sopra . Quindi tutto ciò che devi fare è cambiare il risultato che questo hook produce; invece di un'eccezione, non dovrebbe assolutamente restituire nulla.

Il modo migliore per farlo è usare un wrapper . I wrapper hook non devono svolgere il vero lavoro, ma hanno la possibilità di modificare ciò che accade al risultato di un hook. Tutto quello che devi fare è aggiungere la riga:

outcome = yield

nell'implementazione del wrapper hook e ottieni l'accesso al risultato hook , inclusa l'eccezione di test tramite outcome.excinfo. Questo attributo è impostato su una tupla di (tipo, istanza, traceback) se nel test è stata sollevata un'eccezione. In alternativa, è possibile chiamare outcome.get_result()e utilizzare la try...exceptgestione standard .

Quindi, come si fa a superare un test fallito? Hai 3 opzioni di base:

  • È possibile contrassegnare il test come errore previsto chiamando pytest.xfail()il wrapper.
  • È possibile contrassegnare l'elemento come ignorato , il che finge che il test non sia mai stato eseguito in primo luogo, chiamando pytest.skip().
  • È possibile rimuovere l'eccezione, utilizzando il outcome.force_result()metodo ; imposta qui il risultato su un elenco vuoto (ovvero: l'hook registrato non ha prodotto altro che None) e l'eccezione viene cancellata del tutto.

Quello che usi dipende da te. Assicurati di controllare prima il risultato per i test saltati e previsti, poiché non è necessario gestire quei casi come se il test fallisse. Puoi accedere alle eccezioni speciali sollevate da queste opzioni tramite pytest.skip.Exceptione pytest.xfail.Exception.

Ecco un'implementazione di esempio che contrassegna i test non riusciti che non generano ValueError, come saltati :

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

Quando inserito conftest.pynell'output diventa:

$ 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 ======================

Ho usato la -r abandiera per rendere più chiaro che è test_hamstato saltato ora.

Se si sostituisce la pytest.skip()chiamata con pytest.xfail("[XFAIL] ignoring everything but ValueError"), il test viene contrassegnato come errore previsto:

[ ... ]
XFAIL demo/test_foo.py::test_ham
  reason: [XFAIL] ignoring everything but ValueError
[ ... ]

e usando lo outcome.force_result([])contrassegna come passato:

$ pytest -v demo/test_foo.py  # verbose to see individual PASSED entries
[ ... ]
demo/test_foo.py::test_ham PASSED                                        [ 50%]

Dipende da te quale ritieni più adatto al tuo caso d'uso. Per skip()e xfail()ho imitato il formato del messaggio standard (con il prefisso [NOTRUN]o [XFAIL]), ma sei libero di utilizzare qualsiasi altro formato di messaggio che desideri.

In tutti e tre i casi pytest non aprirà il debugger per i test il cui risultato è stato modificato con questo metodo.

Modifica delle singole affermazioni

Se vuoi modificare i asserttest all'interno di un test , ti stai preparando per molto più lavoro. Sì, questo è tecnicamente possibile, ma solo riscrivendo il codice che Python eseguirà in fase di compilazione .

Quando lo usi pytest, questo è già stato fatto . Pytest riscrive le assertdichiarazioni per darti più contesto quando le tue affermazioni falliscono ; vedere questo post del blog per una buona panoramica di ciò che viene fatto e del _pytest/assertion/rewrite.pycodice sorgente . Si noti che quel modulo è lungo oltre 1k linee e richiede di capire come funzionano gli alberi di sintassi astratti di Python . In tal caso, è possibile eseguire il monkeypatch di quel modulo per aggiungere le proprie modifiche lì, incluso circondare il assertcon un try...except AssertionError:gestore.

Tuttavia , non è possibile semplicemente disabilitare o ignorare le asserzioni in modo selettivo, poiché le istruzioni successive potrebbero facilmente dipendere dallo stato (disposizioni specifiche dell'oggetto, set di variabili, ecc.) Da cui un'asserzione ignorata doveva proteggere. Se un'asserzione verifica che foonon lo è None, quindi un'asserzione successiva si basa foo.barsull'esistenza, quindi ti imbatterai semplicemente in un AttributeErrorlì, ecc. Attenersi al rilancio dell'eccezione, se è necessario seguire questa strada.

Non approfondirò ulteriormente la riscrittura assertsqui, poiché non credo che valga la pena perseguire questo obiettivo, non considerando la quantità di lavoro richiesto e con il debug post mortem che consente di accedere allo stato del test al punto di errore di asserzione comunque .

Nota che se vuoi farlo, non devi usare eval()(che non funzionerebbe comunque, assertè un'istruzione, quindi dovresti usare exec()invece), né dovresti eseguire l'asserzione due volte (che può causare problemi se l'espressione utilizzata nello stato di asserzione è stata modificata). Dovresti invece incorporare il ast.Assertnodo all'interno di un ast.Trynodo e collegare un gestore di eccezione che utilizza un ast.Raisenodo vuoto per sollevare nuovamente l'eccezione rilevata.

Utilizzo del debugger per saltare le dichiarazioni di asserzione.

Il debugger Python in realtà ti consente di saltare le istruzioni , usando il comando j/jump . Se si sa in anticipo che una specifica asserzione vi fallire, è possibile utilizzare questo per bypass esso. È possibile eseguire i test con --trace, che apre il debugger all'inizio di ogni test , quindi emettere un j <line after assert>per saltarlo quando il debugger viene messo in pausa appena prima dell'asserzione.

Puoi persino automatizzare questo. Utilizzando le tecniche di cui sopra è possibile creare un plug-in di debugger personalizzato

  • usa l' pytest_testrun_call()hook per catturare l' AssertionErroreccezione
  • estrae il numero di riga "offensivo" dal traceback, e forse con alcune analisi del codice sorgente determina i numeri di riga prima e dopo l'asserzione richiesta per eseguire un salto riuscito
  • esegue nuovamente il test , ma questa volta utilizzando una Pdbsottoclasse che imposta un punto di interruzione sulla linea prima dell'asserzione ed esegue automaticamente un salto al secondo quando il punto di interruzione viene colpito, seguito da un cproseguimento.

Oppure, invece di attendere il fallimento di un'asserzione, è possibile automatizzare l'impostazione dei punti di interruzione per ciascuno asserttrovato in un test (utilizzando nuovamente l'analisi del codice sorgente, è possibile estrarre banalmente i numeri di riga per i ast.Assertnodi in un AST del test), eseguire il test affermato usando i comandi con script debugger e usare il jumpcomando per saltare l'asserzione stessa. Dovresti fare un compromesso; eseguire tutti i test con un debugger (che è lento in quanto l'interprete deve chiamare una funzione di traccia per ogni istruzione) o applicarlo solo ai test falliti e pagare il prezzo di rieseguire quei test da zero.

Un tale plugin sarebbe molto impegnativo da creare, non scriverò un esempio qui, in parte perché non si adatterebbe comunque in una risposta, e in parte perché non penso che valga la pena . Avrei solo aperto il debugger e fatto il salto manualmente. Un'asserzione non riuscita indica un bug nel test stesso o nel codice sotto test, quindi puoi anche concentrarti solo sul debug del problema.


7

Puoi ottenere esattamente quello che vuoi senza assolutamente alcuna modifica del codice con pytest --pdb .

Con il tuo esempio:

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

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

Non appena un test fallisce, puoi eseguirne il debug con il debugger integrato in Python. Se hai terminato il debug, puoi farlo continuecon il resto dei test.


Questo si interromperà quando il caso del test fallisce o la fase del test fallisce.
Nitesh,

Controlla i documenti collegati: doc.pytest.org/en/latest/…
gnvk,

Ottima idea Ma se usi --pdb, testcase si fermerà ad ogni errore. posso decidere in fase di esecuzione in quale errore desidero mettere in pausa il test case
Nitesh

5

Se stai usando PyCharm, puoi aggiungere un Breakpoint di eccezione per mettere in pausa l'esecuzione ogni volta che un'asserzione fallisce. Selezionare Visualizza punti di interruzione (CTRL-MAIUSC-F8) e aggiungere un gestore eccezioni su-rilancio per AssertionError. Si noti che ciò potrebbe rallentare l'esecuzione dei test.

Altrimenti, se non ti dispiace mettere in pausa alla fine di ogni test fallito (poco prima che si verifichino errori) piuttosto che nel momento in cui l'asserzione fallisce, allora hai alcune opzioni. Si noti tuttavia che a questo punto potrebbero essere già stati eseguiti vari codici di pulizia, come la chiusura dei file aperti nel test. Le opzioni possibili sono:

  1. Puoi dire a pytest di rilasciarti nel debugger in caso di errori usando l' opzione --pdb .

  2. È possibile definire il seguente decoratore e decorare ogni funzione di test rilevante con esso. (A parte la registrazione di un messaggio, puoi anche avviare un pdb.post_mortem a questo punto, o anche un codice interattivo. Interagire con i locali del frame in cui ha avuto origine l'eccezione, come descritto in questa risposta .)

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. Se non si desidera decorare manualmente ogni funzione di test, è possibile invece definire un dispositivo di autouso che ispeziona 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)

Mi è piaciuta la risposta con i decoratori, ma non può essere eseguita in modo dinamico. Voglio controllare dinamicamente quando voglio pause_on_assert o no. C'è qualche soluzione per questo?
Nitesh,

Dinamico in che modo? Come in un singolo interruttore per abilitarlo / disabilitarlo ovunque? O un modo per controllarlo per ogni test?
Uri Granta,

Supponiamo che sto eseguendo alcune prove. Nel mezzo ho avuto la necessità di una pausa in caso di fallimento. Abiliterò l'interruttore. Più tardi, in qualsiasi momento, sento che devo disabilitare l'interruttore.
Nitesh,

Il tuo decoratore in risposta: 2 non funzionerà per me perché il mio caso di test avrà più
asserzioni

Per quanto riguarda uno "switch", è possibile aggiornare l'implementazione di pause_on_assertleggere da un file per decidere se mettere in pausa o meno.
Uri Granta,

4

Una soluzione semplice, se si desidera utilizzare il codice di Visual Studio, potrebbe essere quella di utilizzare i punti di interruzione condizionali .

Ciò ti consentirebbe di impostare le tue asserzioni, ad esempio:

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

Quindi aggiungi un punto di interruzione condizionale nella riga di asserzione che si interromperà solo quando l'asserzione fallisce:

inserisci qui la descrizione dell'immagine


@Nitesh - Penso che questa soluzione risolva tutti i tuoi problemi, rompi solo quando un'asserzione fallisce, puoi eseguire il debug del codice lì e poi e puoi continuare con i test rimanenti in seguito ... Anche se è un po 'più complicato da configurare inizialmente
Nick Martin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.