434 Stimmen

Wie testet man, dass keine Ausnahme geworfen wird?

Ich weiß, dass eine Möglichkeit, dies zu tun, wäre:

@Test
public void foo() {
   try {
      // Führe den Code aus, von dem du keine Ausnahmen erwartest.
   } catch(Exception e) {
      fail("Sollte keine Ausnahme geworfen haben");
   }
}

Gibt es einen saubereren Weg, dies zu tun? (Vielleicht mit Junit's @Rule?)

293voto

oHo Punkte 45335

JUnit 5 (Jupiter) stellt drei Funktionen bereit, um das Vorhandensein/Abwesenheit von Ausnahmen zu überprüfen:

assertAll()

Überprüft, dass alle übergebenen Executable
keine Ausnahmen auslösen.

assertDoesNotThrow()

Überprüft, dass die Ausführung des übergebenen Executable/Supplier
keine Art von Ausnahme auslöst.

Diese Funktion ist verfügbar seit JUnit 5.2.0 (29. April 2018).

assertThrows()

Überprüft, dass die Ausführung des übergebenen Executable
eine Ausnahme des expectedType
auslöst und die Ausnahme zurückgibt.

Beispiel

package test.mycompany.myapp.mymodule;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class MyClassTest {

    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw() {
        String myString = "this string has been constructed";
        assertAll(() -> MyClass.myFunction(myString));
    }

    @Test
    void when_string_has_been_constructed_then_myFunction_does_not_throw__junit_v520() {
        String myString = "this string has been constructed";
        assertDoesNotThrow(() -> MyClass.myFunction(myString));
    }

    @Test
    void when_string_is_null_then_myFunction_throws_IllegalArgumentException() {
        String myString = null;
        assertThrows(
            IllegalArgumentException.class,
            () -> MyClass.myFunction(myString));
    }

}

279voto

Jeroen Vannevel Punkte 42391

Du gehst das falsch an. Teste einfach deine Funktionalität: Wenn eine Ausnahme auftritt, schlägt der Test automatisch fehl. Wenn keine Ausnahme auftritt, werden deine Tests alle grün.

Ich habe bemerkt, dass diese Frage von Zeit zu Zeit Interesse weckt, deshalb werde ich etwas ausführlicher darauf eingehen.

Hintergrund zum Unit-Testing

Wenn du Unit-Tests durchführst, ist es wichtig, für dich selbst zu definieren, was du als eine Arbeitseinheit betrachtest. Grundsätzlich handelt es sich um einen Ausschnitt deiner Codebasis, der möglicherweise mehrere Methoden oder Klassen umfasst, die ein einzelnes Stück Funktionalität repräsentieren.

Oder wie in The Art of Unit Testing, 2. Auflage von Roy Osherove, Seite 11, definiert:

Ein Unit-Test ist ein automatisierter Codeabschnitt, der die getestete Arbeitseinheit aufruft und dann einige Annahmen über ein einzelnes Endergebnis dieser Einheit überprüft. Ein Unit-Test wird fast immer unter Verwendung eines Unit-Test-Frameworks geschrieben. Er kann leicht geschrieben und schnell ausgeführt werden. Er ist verlässlich, lesbar und pflegbar. Er ist in seinen Ergebnissen konsistent, solange sich der Produktionscode nicht geändert hat.

Wichtig zu realisieren ist, dass eine Arbeitseinheit in der Regel nicht nur eine Methode ist, sondern auf grundlegender Ebene eine Methode und danach von anderen Arbeitseinheiten verkapselt wird.

Eingabebildbeschreibung hier eingeben

Idealerweise sollte für jede separate Arbeitseinheit eine Testmethode vorhanden sein, sodass du sofort sehen kannst, wo etwas schiefgeht. In diesem Beispiel gibt es eine grundlegende Methode namens getUserById(), die einen Benutzer zurückgibt, und insgesamt 3 Arbeitseinheiten.

Die erste Arbeitseinheit sollte testen, ob ein gültiger Benutzer im Falle von gültiger und ungültiger Eingabe zurückgegeben wird.
Alle Ausnahmen, die von der Datenquelle ausgelöst werden, müssen hier behandelt werden: Wenn kein Benutzer vorhanden ist, sollte ein Test zeigen, dass eine Ausnahme ausgelöst wird, wenn der Benutzer nicht gefunden werden kann. Ein Beispiel hierfür könnte die IllegalArgumentException sein, die mit der Annotation @Test(expected = IllegalArgumentException.class) erfasst wird.

Nachdem du alle Anwendungsfälle für diese grundlegende Arbeitseinheit behandelt hast, gehst du eine Ebene höher. Hier machst du dasselbe, behandelst jedoch nur die Ausnahmen, die von der Ebene direkt unter der aktuellen stammen. Auf diese Weise bleibt dein Testcode gut strukturiert und ermöglicht es dir, schnell durch die Architektur zu navigieren, um festzustellen, wo etwas schiefgeht, anstatt wild herumzuspringen.

Behandlung gültiger und fehlerhafter Eingaben von Tests

Jetzt sollte klar sein, wie wir diese Ausnahmen behandeln werden. Es gibt 2 Arten von Eingaben: gültige Eingaben und fehlerhafte Eingaben (die Eingabe ist im strengen Sinne gültig, aber nicht korrekt).

Wenn du mit gültigen Eingaben arbeitest, setzt du die implizite Erwartung, dass jeder Test, den du schreibst, funktionieren wird.

Ein solcher Methodenaufruf könnte so aussehen: existingUserById_ShouldReturn_UserObject. Wenn diese Methode fehlschlägt (z.B.: eine Ausnahme ausgelöst wird), weißt du, dass etwas schiefgegangen ist und du kannst mit der Fehlersuche beginnen.

Durch das Hinzufügen eines weiteren Tests (nonExistingUserById_ShouldThrow_IllegalArgumentException), der die fehlerhafte Eingabe verwendet und eine Ausnahme erwartet, kannst du sehen, ob deine Methode tut, was sie mit falscher Eingabe tun soll.

Zusammenfassung

Du hast versucht, zwei Dinge in deinem Test zu überprüfen: gültige und fehlerhafte Eingaben. Indem du dies in zwei Methoden aufteilst, die jeweils eine Sache tun, wirst du viel klarere Tests haben und einen besseren Überblick darüber haben, wo die Fehler auftreten.

Indem du die geschichteten Arbeitseinheiten im Auge behältst, kannst du auch die Anzahl der Tests reduzieren, die du für eine Schicht höher in der Hierarchie benötigst, da du nicht für jedes mögliche Problem in den darunter liegenden Schichten darfst: Die Schichten unterhalb der aktuellen sind eine virtuelle Gewähr dafür, dass deine Abhängigkeiten funktionieren und wenn etwas schief geht, dann liegt es an der aktuellen Schicht (vorausgesetzt, die darunter liegenden Schichten werfen keine Fehler selbst zurück).

260voto

Sven Döring Punkte 3555

Ich bin über dies gestolpert, wegen der SonarQube-Regel "squid:S2699": "Fügen Sie diesem Testfall mindestens eine Behauptung hinzu".

Ich hatte einen einfachen Test, dessen einziges Ziel darin bestand, ohne Ausnahme durchzugehen.

Betrachten Sie diesen einfachen Code:

public class Printer {

    public static void printLine(final String line) {
        System.out.println(line);
    }
}

Welche Art von Behauptung kann diesem Methodentest hinzugefügt werden? Natürlich können Sie eine Try-Catch darum legen, aber das ist nur Code-Blähung.

Die Lösung kommt direkt von JUnit selbst.

Falls keine Ausnahme ausgelöst wird und Sie dieses Verhalten explizit veranschaulichen möchten, fügen Sie einfach expected wie im folgenden Beispiel hinzu:

@Test(expected = Test.None.class /* keine Ausnahme erwartet */)
public void test_printLine() {
    Printer.printLine("line");
}

Test.None.class ist der Standardwert für den erwarteten Wert.

Wenn Sie import org.junit.Test.None, können Sie dann schreiben:

@Test(expected = None.class)

was Sie möglicherweise lesbarer finden werden.

124voto

denu Punkte 1698

Für JUnit-Versionen vor 5:

Mit AssertJ Fluent Assertions 3.7.0:

Assertions.assertThatCode(() -> toTest.method())
    .doesNotThrowAnyException();

Aktualisierung:

JUnit 5 führte die assertDoesNotThrow()-Assertion ein, daher bevorzuge ich es, diese anstelle einer zusätzlichen Abhängigkeit zu Ihrem Projekt hinzuzufügen. Siehe diese Antwort für Details.

33voto

Groostav Punkte 3081

Java 8 macht dies viel einfacher, und Kotlin/Scala doppelt so.

Wir können eine kleine Hilfsklasse schreiben

class MyAssertions{
  public static void assertDoesNotThrow(FailingRunnable action){
    try{
      action.run()
    }
    catch(Exception ex){
      throw new Error("erwartete Aktion sollte nicht werfen, aber tat es doch!", ex)
    }
  }
}

@FunctionalInterface Schnittstelle FailingRunnable { void run() throws Exception }

und dann wird Ihr Code einfach:

@Test
public void foo(){
  MyAssertions.assertDoesNotThrow(() -> {
    //ausführen von Code, von dem Sie erwarten, dass er keine Ausnahmen wirft.
  }
}

Wenn Sie keinen Zugriff auf Java-8 haben, würde ich eine schmerzhaft alte Java-Einrichtung verwenden: beliebige Codeblöcke und einen einfachen Kommentar

//setup
Komponente komponente = new Component();

//act
configure(component);

//assert 
/*assert does not throw*/{
  component.doSomething();
}

Und schließlich, mit Kotlin, einer Sprache, in die ich mich kürzlich verliebt habe:

fun (() -> Any?).shouldNotThrow() 
    = try { invoke() } catch (ex : Exception){ throw Error("erwartet, nicht zu werfen!", ex) }

@Test fun `wenn foo passiert, sollte nicht werfen`(){

  //...

  { /*code, das nicht werfen sollte*/ }.shouldNotThrow()
}

Obwohl es viel Spielraum gibt, wie Sie dies genau ausdrücken möchten, war ich immer ein Fan von flüssigen Behauptungen.


In Bezug auf

Sie gehen dies falsch an. Testen Sie einfach Ihre Funktionalität: Wenn eine Ausnahme geworfen wird, schlägt der Test automatisch fehl. Wenn keine Ausnahme geworfen wird, werden alle Ihre Tests grün.

Dies ist im Prinzip richtig, aber im Fazit falsch.

Java erlaubt Ausnahmen zur Steuerung des Flusses. Dies geschieht durch die JRE-Laufzeit selbst in APIs wie Double.parseDouble über eine NumberFormatException und Paths.get über eine InvalidPathException.

Angenommen, Sie haben eine Komponente geschrieben, die Zahlzeichenketten für Double.ParseDouble validiert, vielleicht mit einem Regex, vielleicht einem handgeschriebenen Parser oder vielleicht etwas, das andere Domänenregeln einbettet, die den Bereich eines Double auf etwas Bestimmtes einschränken, wie testen Sie am besten diese Komponente? Ich denke, ein offensichtlicher Test wäre zu behaupten, dass, wenn die resultierende Zeichenkette geparst wird, keine Ausnahme geworfen wird. Ich würde diesen Test entweder mit dem oben genannten assertDoesNotThrow oder /*kommentieren*/{code} Block schreiben. Etwas wie

@Test public void given_validator_accepts_string_result_should_be_interpretable_by_doubleParseDouble(){
  //setup
  String input = "12.34E+26" //eine Zeichenketten-Doppelzahl mit domänenspezifischer Bedeutung

  //act
  boolean isValid = component.validate(input)

  //assert -- Verwendung der Bibliothek 'assertJ', mein persönlicher Favorit 
  assertThat(isValid).describedAs(input + " wurde von der Komponente als gültig betrachtet").isTrue();
  assertDoesNotThrow(() -> Double.parseDouble(input));
}

Ich würde Sie auch ermutigen, diesen Test auf input zu parameterisieren, indem Sie Theories oder Parameterized verwenden, damit Sie diesen Test einfacher für andere Eingaben wiederverwenden können. Alternativ, wenn Sie exotisch werden möchten, könnten Sie ein Testgenerierungstool (und dies) verwenden. TestNG hat eine bessere Unterstützung für parameterisierte Tests.

Was ich besonders unangenehm finde, ist die Empfehlung, @Test(expectedException=IllegalArgumentException.class) zu verwenden, diese Ausnahme ist gefährlich breit. Ändert sich Ihr Code so, dass der zu testende Komponentenkonstruktor if(constructorArgument <= 0) throw IllegalArgumentException() hat, und Ihr Test 0 für dieses Argument liefert, weil es praktisch war--und das ist sehr üblich, weil das Generieren guter Testdaten ein überraschend schweres Problem ist--, dann wird Ihr Test grün sein, obwohl er nichts testet. Ein solcher Test ist schlechter als nutzlos.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X