Python unittest - opposto di assertRaises?


374

Voglio scrivere un test per stabilire che non viene sollevata un'eccezione in una determinata circostanza.

È semplice verificare se viene sollevata un'eccezione ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... ma come puoi fare il contrario .

Qualcosa del genere è quello che sto cercando ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

6
Puoi sempre semplicemente fare tutto ciò che dovrebbe funzionare nel test. Se genera un errore, verrà visualizzato (conteggiato come un errore, piuttosto che un errore). Naturalmente, ciò presuppone che non generi alcun errore, piuttosto che solo un tipo definito di errore. A parte questo, immagino che dovresti scriverne uno tuo.
Thomas K,

1
possibile duplicato di Python - test che ha
esito

Si scopre infatti che è possibile implementare un assertNotRaisesmetodo che condivide il 90% del suo codice / comportamento con assertRaisescirca 30 righe di codice. Vedi la mia risposta qui sotto per i dettagli.
tel

Lo voglio in modo da poter confrontare due funzioni con hypothesisper assicurarmi che producano lo stesso output per tutti i tipi di input, ignorando i casi in cui l'originale solleva un'eccezione. assume(func(a))non funziona perché l'output può essere un array con ambiguo valore di verità. Quindi voglio solo chiamare una funzione e ottenere Truese non fallisce. assume(func(a) is not None)opere suppongo
endolith

Risposte:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon - No, questa è la soluzione corretta in effetti. La soluzione proposta da user9876 è concettualmente imperfetta: se si verifica il non rilancio di dire ValueError, ma ValueErrorviene invece sollevato, il test deve uscire con una condizione di errore, non di errore. D'altra parte, se nell'esecuzione dello stesso codice si generasse a KeyError, sarebbe un errore, non un errore. In Python - diversamente da alcuni altri linguaggi - Le eccezioni sono usate abitualmente per il flusso di controllo, ecco perché abbiamo except <ExceptionName>davvero la sintassi. A tal proposito, la soluzione di user9876 è semplicemente sbagliata.
mac,

@mac - Anche questa è una soluzione corretta? stackoverflow.com/a/4711722/6648326
MasterJoe2

Questo ha lo sfortunato effetto di mostrare una copertura <100% (l'eccezione non accadrà mai) per i test.
Shay,

3
@Shay, IMO dovresti sempre escludere i file di test stessi dai rapporti di copertura (poiché quasi sempre eseguono il 100% per definizione, gonferesti artificialmente i rapporti)
Original BBQ Sauce

@ original-bbq-sauce, non mi lascerebbe aperto ai test involontariamente saltati. Ad esempio, errore di battitura nel nome del test (ttst_function), configurazione di esecuzione errata in pycharm, ecc.?
Shay,

67

Ciao - Voglio scrivere un test per stabilire che non viene sollevata un'eccezione in una determinata circostanza.

Questo è il presupposto predefinito: non vengono sollevate eccezioni.

Se non dici altro, questo è assunto in ogni singolo test.

Non è necessario scrivere alcuna affermazione per questo.


7
@IndradhanushGupta Beh, la risposta accettata rende il test più pitonico di questo. Esplicito è meglio che implicito.
0xc0de,

17
Nessun altro commentatore ha sottolineato il motivo per cui questa risposta è errata, sebbene sia la stessa ragione per cui la risposta di user9876 è errata: guasti ed errori sono cose diverse nel codice di test. Se la tua funzione dovesse generare un'eccezione durante un test che non afferma, il framework di test lo considererebbe un errore , piuttosto che un fallimento nel non affermarlo.
coredumperror,

@CoreDumpError Comprendo la differenza tra un errore e un errore, ma questo non ti costringerebbe a circondare ogni test con una costruzione try / exception? O consiglieresti di farlo solo per i test che generano esplicitamente un'eccezione in alcune condizioni (il che significa sostanzialmente che l'eccezione è prevista ).
Federicojasson,

4
@federicojasson Hai risposto abbastanza bene alla tua domanda in quella seconda frase. Errori e guasti nei test possono essere descritti in modo sintetico rispettivamente come "crash imprevisti" vs "comportamento involontario". Desideri che i tuoi test mostrino un errore quando la tua funzione si arresta in modo anomalo, ma non quando viene generata un'eccezione che sai che viene generata dato che determinati input vengono generati quando vengono dati input diversi.
coredumperror,

52

Basta chiamare la funzione. Se genera un'eccezione, il framework di unit test contrassegnerà questo come errore. Potresti aggiungere un commento, ad esempio:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

35
Fallimenti ed errori sono concettualmente diversi. Inoltre, poiché in Python le eccezioni vengono utilizzate di routine per il flusso di controllo, questo renderà molto molto difficile da capire a colpo d'occhio (= senza esplorare il codice di test) se hai rotto la tua logica o il tuo codice ...
mac

1
O il test ha esito positivo oppure no. Se non passa, dovrai risolverlo. Se viene segnalato come "errore" o "errore" è per lo più irrilevante. C'è una differenza: con la mia risposta vedrai la traccia dello stack in modo da poter vedere dove è stato lanciato PathIsNotAValidOne; con la risposta accettata non avrai tali informazioni, quindi il debug sarà più difficile. (Supponendo Py2; non sono sicuro che Py3 sia migliore in questo).
user9876

19
@ user9876 - No. Le condizioni di uscita del test sono 3 (pass / nopass / error), non 2 come sembra credere erroneamente. La differenza tra errori e guasti è sostanziale e trattarli come se fossero gli stessi è solo una cattiva programmazione. Se non mi credi, dai un'occhiata a come funzionano i test runner e quali alberi decisionali implementano per fallimenti ed errori. Un buon punto di partenza per Python è il xfaildecoratore di Pytest.
mac,

4
Immagino che dipenda da come usi i test unitari. Il modo in cui il mio team utilizza i test unitari deve superare tutti. (Programmazione agile, con una macchina di integrazione continua che esegue tutti i test unitari). So che il test case può riportare "pass", "fail" o "error". Ma ad alto livello ciò che conta davvero per il mio team è "superano tutti i test unitari?" (cioè "è Jenkins verde?"). Quindi, per il mio team, non esiste alcuna differenza pratica tra "fail" e "error". Potresti avere requisiti diversi se usi i test unitari in modo diverso.
user9876

1
@ user9876 la differenza tra 'fail' e 'error' è la differenza tra "il mio assert fallito" e "il mio test non arriva nemmeno all'asserzione". Questa, per me, è una distinzione utile durante i test di riparazione, ma immagino, come dici tu, non per tutti.
CS

14

Sono il poster originale e ho accettato la risposta di cui sopra DGH senza averlo prima utilizzato nel codice.

Una volta che ho usato mi sono reso conto che aveva bisogno di un po 'di modifiche per fare davvero quello che mi serviva (per essere onesti con DGH ha detto "o qualcosa di simile"!).

Ho pensato che valesse la pena pubblicare il tweak qui a beneficio di altri:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

Quello che stavo tentando di fare qui era assicurarmi che se si fosse tentato di creare un'istanza di un oggetto Applicazione con un secondo argomento di spazi, sarebbe stato sollevato pySourceAidExceptions.PathIsNotAValidOne.

Credo che l'uso del codice sopra riportato (basato pesantemente sulla risposta della DGH) lo farà.


2
Dato che stai chiarendo la tua domanda e non stai rispondendo, dovresti averla modificata (senza risposta). Per favore vedi la mia risposta qui sotto.
Hiwaylon,

13
Sembra essere esattamente l'opposto del problema originale. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)dovrebbe fare il lavoro in questo caso.
Antony Hatchkins,

8

È possibile definire assertNotRaisesriutilizzando circa il 90% dell'implementazione originale di assertRaisesnel unittestmodulo. Con questo approccio, si finisce con un assertNotRaisesmetodo che, a parte la sua condizione di fallimento inversa, si comporta in modo identicoassertRaises .

TLDR e demo live

Risulta sorprendentemente facile aggiungere un assertNotRaisesmetodo a unittest.TestCase(mi ci è voluto circa 4 volte più tempo per scrivere questa risposta come ha fatto il codice). Ecco una demo live del assertNotRaisesmetodo in azione . Proprio comeassertRaises , puoi passare un callable e discutere assertNotRaises, oppure puoi usarlo in una withdichiarazione. La demo live include casi di test che lo dimostranoassertNotRaises funziona come previsto.

Dettagli

L'implementazione di assertRaisesaunittest è abbastanza complicata, ma con un po 'di sottoclasse intelligente puoi sovrascrivere e invertire la sua condizione di fallimento.

assertRaisesè un metodo breve che fondamentalmente crea solo un'istanza della unittest.case._AssertRaisesContextclasse e la restituisce (vedere la sua definizione nel unittest.casemodulo). Puoi definire la tua _AssertNotRaisesContextclasse effettuando la sottoclasse _AssertRaisesContexte sovrascrivendo il suo __exit__metodo:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Normalmente si definiscono le classi di test case facendole ereditare da TestCase. Se invece erediti da una sottoclasse MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

tutti i casi di test avranno ora il assertNotRaisesmetodo a loro disposizione.


Da dove viene la tracebacktua elsedichiarazione?
NOh,

1
@NOhs Mancava import. È fisso
tel

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

potrebbe essere modificato se è necessario accettare i parametri.

chiama come

self._assertNotRaises(IndexError, array, 'sort')

1

Ho trovato utile patch di scimmia unittestcome segue:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

Questo chiarisce l'intento durante il test per l'assenza di un'eccezione:

self.assertMayRaise(None, does_not_raise)

Questo semplifica anche i test in un ciclo, che spesso mi ritrovo a fare:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

Cos'è una patch scimmia?
ScottMcC,

1
Vedi en.wikipedia.org/wiki/Monkey_patch . Dopo aver aggiunto assertMayRaiseal unittest.TestSuitesi può semplicemente far finta che di parte della unittestbiblioteca.
AndyJost,

0

Se si passa una classe di eccezione a assertRaises(), viene fornito un gestore di contesto. Questo può migliorare la leggibilità dei tuoi test:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Ciò consente di testare casi di errore nel codice.

In questo caso, si sta verificando che PathIsNotAValidOneviene generato quando si passano parametri non validi al costruttore dell'applicazione.


1
No, ciò fallirà solo se l'eccezione non viene generata nel blocco del gestore di contesto. Può essere facilmente testato da 'with self.assertRaises (TypeError): raise TypeError', che passa.
Matthew Trevor,

@MatthewTrevor Buona chiamata. Come ricordo, anziché testare il codice eseguito correttamente, cioè non aumentare, stavo suggerendo di testare casi di errore. Ho modificato la risposta di conseguenza. Spero di poter guadagnare un +1 per uscire dal rosso. :)
hiwaylon,

Nota, anche questo è Python 2.7 e versioni
qneill

0

puoi provare così. try: self.assertRaises (None, function, arg1, arg2) ad eccezione di: pass se non si inserisce codice all'interno di try block lo farà tramite l'eccezione "AssertionError: None not raised" e il caso di test fallirà. Il caso di test verrà superato se inserito dentro prova a bloccare quale è il comportamento previsto.


0

Un modo semplice per garantire che l'oggetto venga inizializzato senza errori è testare l'istanza del tipo di oggetto.

Ecco un esempio:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
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.