Blog

JUnit Rules

Dieser Beitrag soll auf ein Feature von JUnit hinweisen, das bereits seit der Version 4.7 (also seit ca. zwei Jahren)  besteht, aber das ich bis vor kurzem leider nicht kannte. Vielleicht geht es dem geneigten Leser ja genauso – und dann lohnt es sich weiterzulesen, versprochen!

Zum ersten Mal gehört von JUnit Rules habe  ich von Kent Beck selbst (nein, nicht persönlich, sondern in diesem Podcast :D). Für die zwei Leute auf der Welt, die Unittests schreiben, aber nicht wissen, wer dieser Kent Beck sein soll, empfehle ich den Konsum des erwähnten Podcasts, da stellt sich der Großmeister am Anfang auch vor und erzählt noch andere wissenswerte Dinge über das Thema testgetriebene Entwicklung.

Kent Beck beschreibt JUnit Rules dort mit diesen Worten:

Kent Beck

You should be able to just kind of read a test and it tells a story. And rules are a way of setting the stage of a test in a way that is obvious […] but at the same time can be quite simple!

Hört sich verlockend an – nicht wahr?

Aber wie sieht das denn nun konkret aus?

Schauen wir uns einmal diesen Test an:

@Test
public void checkException(){
   try{
      callMethod("foobar");
      fail("No exception thrown!");
   }
   catch(IllegalArgumentException e){
      assertEquals("foobar ist kein legaler Parameter!",e.getMessage());
   }
}

Ein Test, wie er sicherlich von jedem von uns schon x-mal geschrieben wurde: Stellt sicher, dass der richtige Exceptiontyp mit der korrekten Beschreibung die Reaktion auf einen falschen Parameter ist. Rein funktional ist nichts falsch an diesem Test. Er hat allerdings ein kleines Problem: Schön zu lesen ist er nicht gerade…

Jetzt schauen wir mal, ob wir denselben Test nicht mit Hilfe von Rules ausdrucksstärker gestalten können:

@Rule
public ExpectedException exception = ExpectedException.none();

@Test
public void checkException() {
   exception.expect(IllegalArgumentException.class);
   exception.expectMessage("foobar ist kein legaler Parameter!");
   callMethod("foobar");
}

Diese Version des Tests macht genau dasselbe wie die ohne Rules. Trotzdem ist der Testfall einfacher zu erfassen: Ich würde sogar behaupten, dass man sich nicht mal mit Java auskennen muss, um zu wissen, was hier vor sich geht. Kent hat also Recht: Rules sind ein mächtiges Mittel für sehr deskriptive, ausdruckstarke Tests.

Bisher kennen wir eine Rule: ExpectedException. Mit dieser Rule läßt sich anscheinend die Interpretation des Tests durch JUnit verändern – denn der Test schlägt nicht fehl, selbst wenn eine Exception geworfen wird – was normalerweise nicht der Fall ist. Schauen wir uns ein paar weitere Rules an, die sich im Lieferumfang von JUnit befinden:

  • ErrorCollector
  • TemporaryFolder
  • TestName
  • Timeout

Da die Rules alle recht sprechende Namen besitzen, dürfte klar sein, wofür sie gut sind – und falls nicht: Ein Blick in die Dokumentation der Klasse – und es herrscht Klarheit. Wie funktioniert aber nun das Ganze? Auch diese Frage lässt sich recht einfach beantworten, wenn wir uns anschauen, was alle mitgelieferten Rules gemeinsam haben. Alle implementieren nämlich das Interface MethodRule:

public interface MethodRule {

   Statement apply(Statement base, FrameworkMethod method, Object target);
}

Ein Statement führt einen Test aus. Oder ausgedrückt in Quellcode:

public abstract class Statement {
   public abstract void evaluate() throws Throwable;
}

Also gibt einem MethodRule die Möglichkeit, das Statement, das JUnit normalerweise benutzt, in ein eigenes Statement zu wrappen, das sich ein klein bisschen anders verhält – indem es zum Beispiel bestimmte Exceptions fängt und deren Messages überprüft. Kenner des Decorator Patterns können also Wiedersehensfreude feiern.

Wenn man Rules benutzt, kommt man übrigens noch in den Genuss eines weiteren Designkniffs: Der Verwendung von Komposition statt Vererbung. Man muss nun nicht mehr seine Testklassen danach zusammenstellen, welche Vorarbeiten oder Nacharbeiten zu verrichten sind (um diese dann in entsprechenden Before und After Methoden oder TestRunner zu definieren), sondern kann sie so zusammenfügen, dass sie – wie war das nochmal? Ach ja, genau: Die Geschichte erzählen.

Danke, Kent!

Holisticon AG — Teile diesen Artikel

Über den Autor

4 Kommentare

  1. Hi, bin durch Zufall auf diesen Artikel gestossen.
    Stimmt, ich finde den Test an sich erst mal lesbarer als dieser
    riesige Try/Catch Block.
    Ich habe aber öfters in einem Test mehrere Fälle abgedeckt,
    so dass ich dort z.B die „illegale“ Konstruktion von Objekten teste
    und deshalb auch Einzeiler schreibe.
    Muss aber Eclipse am Formatieren hindern // @formatter:off
    Z.B
    try { foo = new Foo(null, null, null); fail(); } catch (IllegalArgumentException e) {…};
    try { foo = new Foo(„A1“, null, null); fail(); } catch (IllegalArgumentException e) {…};
    try { foo = new Foo(„A1“, „A2“, null); fail(); } catch (IllegalArgumentException e) {…};

    Mir ist aber nicht klar, wie ich dass jetzt mit der Rule bzw.
    dem ExpectedException Objekt abbilden kann.
    Ideen?

  2. Hallo uvula,

    das passt ja hervorragend, genau einen solchen Fall bearbeite ich auch gerade. Ich halte es für lesbarer und später auch besser erweiter- bzw. anpassbar, jeden Test in eine eigene Test-Methode auszulagern und diesen sprechende Bezeichner zu geben. Etwa so:

    @Test
    public void testFoo_WithA1Null() {
      exception.expect(IllegalArgumentExcpetion.class);
      foo = new Foo(null, null, null)
    }
    
    public void testFoo_WithA2Null() {
      exception.expect(IllegalArgumentExcpetion.class);
      foo = new Foo("A1", null, null)
    }
    
    

    Ist nur unwesentlich mehr Arbeit, dafür aber extrem transparent und gut isoliert. Hilft das weiter?

  3. Hi,
    das mit den separaten Methoden war mir eigentlich klar.
    Und wenn ich auch mal darüber nachgedacht hätte :-), wäre mir
    auch sicherlich klar geworden, dass es in Java so eigentlich
    gar nicht sinnvoll wäre den folgenden Code hinzuschreiben.

    exception.expect(IllegalArgumentExcpetion.class);
    foo = new Foo(null, null, null); // <– Exception erwartet
    exception.expect(IllegalArgumentExcpetion.class);
    foo = new Foo("A1", null, null); // <– Exception erwartet

    Denn jede Exception würde ja die Ausführung der nachfolgenden Zeilen
    unterbinden. Somit müsste man natürlich auch hier wieder Try/Catch
    einbringen, was der Übersichtlichkeit schadet.
    Oder gibt es doch einen Weg?

    (Ja, und die Datenstruktur ExpectedException ist dafür auch nicht ausgelegt,
    die Exceptions zu sammeln).

    Ich kenne AOP nur sehr rudimentär, damit könnte man aber vielleicht
    ein elegante Lösung zaubern, denke aber das wäre mit Kanonen gegen
    Spatzen geschossen. Dann lieber try/catch oder viele Methoden.

    BTW: ich hätte erwartet, dass mir diese Portal hier eine E-Mail sendet, wenn jemand den
    Artikel komentiert.

  4. Hi uvula,

    was stört Dich den an dem Bsp. in Jan’s Kommentar?
    Es sind ja zwei verschiede Testfälle. Und jedem eine eigene
    Methode zu spendieren ist gute Praxis und hat viele Vorteile.

    Danke für Deinen Beitrag und weiterhin happy testing!

Antwort hinterlassen