Python unittest - Gegenteil von assertRaises?


374

Ich möchte einen Test schreiben, um festzustellen, dass eine Ausnahme unter bestimmten Umständen nicht ausgelöst wird.

Es ist einfach zu testen, ob eine Ausnahme ausgelöst wird ...

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

... aber wie kann man das Gegenteil tun .

So etwas wie das, wonach ich suche ...

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

6
Sie können immer einfach das tun, was im Test funktionieren soll. Wenn ein Fehler ausgelöst wird, wird dieser angezeigt (als Fehler und nicht als Fehler gezählt). Dies setzt natürlich voraus, dass kein Fehler ausgelöst wird, sondern nur ein definierter Fehlertyp. Davon abgesehen müssten Sie wohl Ihre eigenen schreiben.
Thomas K


Es stellt sich heraus, dass Sie tatsächlich eine assertNotRaisesMethode implementieren können, die 90% ihres Codes / Verhaltens mit assertRaisesungefähr 30 Codezeilen teilt. Siehe meine Antwort unten für Details.
Tel.

Ich möchte dies, damit ich zwei Funktionen vergleichen kann hypothesis, um sicherzustellen, dass sie für alle Arten von Eingaben dieselbe Ausgabe erzeugen, während ich die Fälle ignoriere, in denen das Original eine Ausnahme auslöst. assume(func(a))funktioniert nicht, da die Ausgabe ein Array mit einem mehrdeutigen Wahrheitswert sein kann. Ich möchte also nur eine Funktion aufrufen und abrufen, Truewenn sie nicht fehlschlägt. assume(func(a) is not None)funktioniert ich denke
Endolith

Antworten:


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

32
@hiwaylon - Nein, das ist tatsächlich die richtige Lösung. Die von user9876 vorgeschlagene Lösung ist konzeptionell fehlerhaft: Wenn Sie testen, ob beispielsweise nicht ausgelöst wurde ValueError, aber ValueErrorstattdessen ausgelöst wird , muss Ihr Test mit einer Fehlerbedingung beendet werden, nicht mit einer Fehlerbedingung. Wenn Sie dagegen denselben Code ausführen würden KeyError, wäre dies ein Fehler und kein Fehler. In Python werden - anders als in einigen anderen Sprachen - Ausnahmen routinemäßig für den Kontrollfluss verwendet. Deshalb haben wir in der except <ExceptionName>Tat die Syntax. In dieser Hinsicht ist die Lösung von user9876 einfach falsch.
Mac

@mac - Ist das auch eine richtige Lösung? stackoverflow.com/a/4711722/6648326
MasterJoe2

Dieser hat den unglücklichen Effekt, dass für Tests eine Abdeckung von <100% angezeigt wird (die Ausnahme wird niemals eintreten).
Shay

3
@Shay, IMO sollten Sie immer die Testdateien selbst aus den Berichterstattungsberichten ausschließen (da sie per Definition fast immer zu 100% ausgeführt werden, würden Sie die Berichte künstlich aufblasen)
Original BBQ Sauce

@ original-bbq-soße, würde mich das nicht für ungewollt übersprungene Tests offen lassen. ZB Tippfehler im Testnamen (ttst_function), falsche Laufkonfiguration in pycharm usw.?
Shay

67

Hallo, ich möchte einen Test schreiben, um festzustellen, dass unter bestimmten Umständen keine Ausnahme ausgelöst wird.

Dies ist die Standardannahme - Ausnahmen werden nicht ausgelöst.

Wenn Sie nichts anderes sagen, wird dies in jedem einzelnen Test vorausgesetzt.

Sie müssen dafür keine Behauptung schreiben.


7
@IndradhanushGupta Nun, die akzeptierte Antwort macht den Test pythonischer als diesen. Explizit ist besser als implizit.
0xc0de

17
Kein anderer Kommentator hat darauf hingewiesen, warum diese Antwort falsch ist, obwohl es der gleiche Grund ist, warum die Antwort von user9876 falsch ist: Fehler und Irrtümer sind verschiedene Dinge im Testcode. Wenn Ihre Funktion war eine Ausnahme während eines Tests zu werfen , die nicht geltend macht, würde der Test - Framework , dass als behandeln Fehler , anstatt ein Fehler nicht behaupten.
Coredumperror

@CoreDumpError Ich verstehe den Unterschied zwischen einem Fehler und einem Fehler, aber würde dies Sie nicht zwingen, jeden Test mit einer Try / Exception-Konstruktion zu umgeben? Oder würden Sie empfehlen, dies nur für Tests zu tun, die unter bestimmten Bedingungen explizit eine Ausnahme auslösen (was im Grunde bedeutet, dass die Ausnahme erwartet wird ).
Federicojasson

4
@federicojasson Du hast deine eigene Frage in diesem zweiten Satz ziemlich gut beantwortet. Fehler gegen Fehler in Tests können kurz und bündig als "unerwartete Abstürze" gegen "unbeabsichtigtes Verhalten" beschrieben werden. Sie möchten, dass Ihre Tests einen Fehler anzeigen, wenn Ihre Funktion abstürzt, aber nicht, wenn eine Ausnahme, von der Sie wissen, dass sie bei bestimmten Eingaben ausgelöst wird, bei unterschiedlichen Eingaben ausgelöst wird.
Coredumperror

52

Rufen Sie einfach die Funktion auf. Wenn eine Ausnahme ausgelöst wird, kennzeichnet das Unit-Test-Framework dies als Fehler. Möglicherweise möchten Sie einen Kommentar hinzufügen, z.

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

35
Fehler und Irrtümer sind konzeptionell unterschiedlich. Da in Python routinemäßig Ausnahmen für den Kontrollfluss verwendet werden, ist dies auf einen Blick sehr schwer zu verstehen (= ohne den Testcode zu untersuchen), wenn Sie Ihre Logik oder Ihren Code gebrochen haben ...
mac

1
Entweder besteht Ihr Test oder nicht. Wenn es nicht erfolgreich ist, müssen Sie es reparieren. Ob es sich um einen "Fehler" oder einen "Fehler" handelt, spielt keine Rolle. Es gibt einen Unterschied: Mit meiner Antwort sehen Sie die Stapelverfolgung, sodass Sie sehen können, wohin PathIsNotAValidOne geworfen wurde. Mit der akzeptierten Antwort haben Sie diese Informationen nicht, so dass das Debuggen schwieriger wird. (Angenommen, Py2; nicht sicher, ob Py3 dies besser kann).
user9876

19
@ user9876 - Nein. Die Test-Exit-Bedingungen sind 3 (Pass / Nopass / Error) und nicht 2, wie Sie fälschlicherweise glauben. Der Unterschied zwischen Fehlern und Ausfällen ist erheblich und es ist nur eine schlechte Programmierung, sie so zu behandeln, als wären sie gleich. Wenn Sie mir nicht glauben, schauen Sie sich einfach um, wie Testläufer funktionieren und welche Entscheidungsbäume sie für Fehler und Irrtümer implementieren. Ein guter Ausgangspunkt für Python ist der xfailDekorateur in pytest.
Mac

4
Ich denke, es hängt davon ab, wie Sie Unit-Tests verwenden. So wie mein Team Unit-Tests verwendet, müssen sie alle bestehen. (Agile Programmierung mit einer kontinuierlichen Integrationsmaschine, die alle Komponententests ausführt). Ich weiß, dass der Testfall "bestanden", "nicht bestanden" oder "fehlerhaft" melden kann. Auf hohem Niveau ist es für mein Team jedoch wichtig, dass alle Unit-Tests bestanden werden. (dh "ist Jenkins grün?"). Für mein Team gibt es also keinen praktischen Unterschied zwischen "Fehler" und "Fehler". Möglicherweise haben Sie andere Anforderungen, wenn Sie Ihre Komponententests auf andere Weise verwenden.
user9876

1
@ user9876 Der Unterschied zwischen 'Fehler' und 'Fehler' ist der Unterschied zwischen "Mein Assert ist fehlgeschlagen" und "Mein Test erreicht nicht einmal den Assert". Das ist für mich eine nützliche Unterscheidung bei Fixing-Tests, aber ich denke, wie Sie sagen, nicht für jeden.
CS

14

Ich bin das Originalplakat und habe die obige Antwort von DGH akzeptiert, ohne sie zuvor im Code verwendet zu haben.

Als ich es benutzte, wurde mir klar, dass es ein wenig optimiert werden musste, um das zu tun, wofür ich es brauchte (um der DGH gegenüber fair zu sein, sagte er / sie "oder ähnliches"!).

Ich dachte, es lohnt sich, den Tweak hier zum Nutzen anderer zu veröffentlichen:

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

Ich habe hier versucht sicherzustellen, dass pySourceAidExceptions.PathIsNotAValidOne ausgelöst wird, wenn versucht wird, ein Anwendungsobjekt mit einem zweiten Argument von Leerzeichen zu instanziieren.

Ich glaube, dass die Verwendung des obigen Codes (der stark von der Antwort der DGH abhängt) dies tun wird.


2
Da Sie Ihre Frage klären und nicht beantworten, sollten Sie sie bearbeitet (nicht beantwortet) haben. Bitte sehen Sie meine Antwort unten.
Hiwaylon

13
Es scheint das Gegenteil des ursprünglichen Problems zu sein. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)sollte den Job in diesem Fall machen.
Antony Hatchkins

8

Sie können definieren, assertNotRaisesindem Sie etwa 90% der ursprünglichen Implementierung von assertRaisesim unittestModul wiederverwenden . Mit diesem Ansatz erhalten Sie eineassertNotRaises Methode, die sich, abgesehen von ihrem umgekehrten Fehlerzustand, identisch verhält assertRaises.

TLDR und Live-Demo

Es stellt sich als überraschend einfach heraus, eine assertNotRaisesMethode hinzuzufügen unittest.TestCase(ich habe ungefähr viermal so lange gebraucht, um diese Antwort zu schreiben wie den Code). Hier ist eine Live-Demo der assertNotRaisesMethode in Aktion . Genau wieassertRaises können Sie entweder eine aufrufbare und args passieren zu assertNotRaises, oder Sie können es in einer verwenden withAussage. Die Live-Demo enthält Testfälle, die dies demonstrierenassertNotRaises wie beabsichtigt funktioniert.

Einzelheiten

Die Umsetzung von assertRaisesinunittest ist ziemlich kompliziert, aber mit ein wenig cleverer Unterklasse können Sie den Fehlerzustand überschreiben und umkehren.

assertRaisesist eine kurze Methode, die im Grunde nur eine Instanz der unittest.case._AssertRaisesContextKlasse erstellt und zurückgibt (siehe Definition im unittest.caseModul). Sie können Ihre eigene _AssertNotRaisesContextKlasse definieren, indem Sie _AssertRaisesContextihre __exit__Methode unterordnen und überschreiben :

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

Normalerweise definieren Sie Testfallklassen, indem Sie sie erben lassen TestCase. Wenn Sie stattdessen von einer Unterklasse erben 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

Allen Ihren Testfällen steht jetzt die assertNotRaisesMethode zur Verfügung.


Woher kommt tracebackdeine elseAussage?
NOhs

1
@NOhs Es fehlte ein import. Es ist fest
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)) 

kann geändert werden, wenn Sie Parameter akzeptieren müssen.

Anruf wie

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

1

Ich fand es nützlich, Affen unittestwie folgt zu patchen :

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

Dies verdeutlicht die Absicht beim Testen auf das Fehlen einer Ausnahme:

self.assertMayRaise(None, does_not_raise)

Dies vereinfacht auch das Testen in einer Schleife, was ich oft mache:

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

Was ist ein Affenfeld?
ScottMcC

1
Siehe en.wikipedia.org/wiki/Monkey_patch . Nach der Zugabe assertMayRaisezu unittest.TestSuiteIhnen kann es nur einen Teil der so tut unittestBibliothek.
AndyJost

0

Wenn Sie eine Ausnahmeklasse übergeben, assertRaises()wird ein Kontextmanager bereitgestellt. Dies kann die Lesbarkeit Ihrer Tests verbessern:

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

Auf diese Weise können Sie Fehlerfälle in Ihrem Code testen.

In diesem Fall testen Sie das PathIsNotAValidOnewird ausgelöst, wenn Sie ungültige Parameter an den Anwendungskonstruktor übergeben.


1
Nein, dies schlägt nur fehl, wenn die Ausnahme nicht im Kontextmanagerblock ausgelöst wird. Kann leicht getestet werden durch 'mit self.assertRaises (TypeError): Raise TypeError', das bestanden wird.
Matthew Trevor

@ MatthewTrevor Guter Anruf. Soweit ich mich erinnere, schlug ich vor, Fehlerfälle zu testen, anstatt den Code korrekt auszuführen, dh nicht zu erhöhen. Ich habe die Antwort entsprechend bearbeitet. Hoffentlich kann ich mir +1 verdienen, um aus den roten Zahlen herauszukommen. :)
Hiwaylon

Beachten Sie, dass dies auch Python 2.7 und höher ist: docs.python.org/2/library/…
qneill

0

Sie können so versuchen. try: self.assertRaises (None, function, arg1, arg2) außer: pass Wenn Sie keinen Code in den try-Block einfügen, wird die Ausnahme 'AssertionError: None not ausgelöst "und der Testfall wird fehlgeschlagen. Der Testfall wird bestanden Wenn es in den Try-Block gestellt wird, ist das erwartete Verhalten.


0

Eine einfache Möglichkeit, um sicherzustellen, dass das Objekt fehlerfrei initialisiert wird, besteht darin, die Typinstanz des Objekts zu testen.

Hier ist ein Beispiel :

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.