JUnit (in der Version >=4.8) ist das Testing Framework, was ich in nahezu allen meinen Projekten verwende.

Das soll nicht heißen, dass es das beste ist, aber ich bin vor vielen Jahren zum ersten und letzten Mal mit TestNG in Berührung gekommen und sonst mit keinem anderen Framework.

Um Tests für den eigenen Code zu schreiben, braucht es aus Praktikabilitätsgründen ein eigenes Tool bzw. Framework. Andernfalls müsste man die Umgebung eigenen Code in Tests laufen zu lassen und die entsprechenden Reportings selbst implementieren und das ist aufwendig.

Ein solches Framework ist JUnit, das von Erich Gamma und Kent Beck (dem Erfinder von TDD), entwickelt wurde.

In JUnit können Tests, die als Methoden einer Testklasse ausgeführt werden, gelingen oder misslingen (Failure/Error). Dabei unterschiedet man die Fehlschläge in erwartete (Failures) und unerwartete (Errors). Die erwarteten Fehler sind dabei vom Typ

junit.framework.AssertionFailedError

Schreibt man beispielsweise in Eclipse einen einfachen JUnit-Test, so kann die drei Ergebnisse (u.a. farblich) unterscheiden:

Erfolgreicher JUnit Test (grün)
Erfolgreicher Test (Grüner Balken)
Fehlgeschlagener JUnit Test (rot)
Fehlgeschlagener Test (roter Balken)
Unerwartet fehlgeschlagener JUnit Test (rot)
Unerwartet fehlgeschlagener Test (roter Balken)

Es ist auf den ersten Blick nicht leicht zu erkennen, dass es in Eclipse auch eine textuelle Auswertung gibt, nämlich die Headline (Runs: 1/1, Errors: 0, Failures: 0). Ich bin erst durch einen farbenblinden Kollege darauf aufmerksam geamcht worden, dass die Unterscheidung in rote und grüne Balken nicht barrierefrei ist.

Dennoch hat sich dieses farbliche Merkmal so durchgesetzt, dass die entsprechenden Phasen des TDD nun auch Green-Red-Refactor heißen und nicht etwa Failure-Success-Refactor.

Normalerweise besteht ein Test aus 3 (max. 4) Steps:

  • Test Preparation / Test Fixture / Vorbereitung
  • Test Execution / Ausführung
  • Test Assertions & Verification / Überprüfungen
  • [Test Tear down / Aufräumarbeiten]

Der letzt genannte entfällt in sehr vielen Fällen, nämlich überall da, wo der Garbage Collector von Java seine Arbeit automatisiert verrichtet.

Test Fixture

In der Vorbereitungsphase lege ich mir alle Testdaten und ~parameter inkl. des zu testenden Objekts (SUT – subject under test) zurecht. Hierbei kommt es häufig zum Einsatz von Mocks & Stubs und dabei weiteren Frameworks.

Tests sollten nach Möglichkeit unabhängig von anderen Tests und Funktionsbausteinen gebaut sein, was nicht ausschließt, dass entsprechende Komponenten extra für Tests gebaut werden. Beispielsweise werden in diesem Bereich häufig sogenannte Builder eingesetzt, um Testdatenobjekte mit verschiedenen Werten zu erzeugen. Vor allem aber aus Gründen der Lesbarkeit werden Builder – Objekte hier benutzt.

Sehr häufig gibt es auch die Situation, dass ein Test mit beliebig vielen unterschiedlichen Werten ausgeführt werden soll. Man spricht hier von einem parametrisierten Test.

Test Execution

Zur Ausführung des Tests ist nicht viel zu sagen, außer: Do it.
Das ausführende und damit zu testende Objekt heißt bei vielen sut (subject under test), aber das ist „nur“ eine – aus meiner Sicht zweitrangige – Namenskonvention.

Test Assertions & Verifications

Das „Herzstück“ des Tests ist tatsächlich der Bereich der Überprüfungen und damit auch der Bereich, wo bisher am meisten Gehirnschmalz reingesteckt wurde. Allen voran wurden die bis zur Version 4.4 gültigen assertEquals – Methoden durch assertThat – Methoden des Frameworks Hamcrest ersetzt.

Assertions

Aber vorher einen Schritt zurück. Es gibt ca. 50 unterschiedliche assert-Methoden.

Eine assert-Methode prüft (bzw. korrekt übersetzt stellt sicher), dass eine Bedingung erfüllt ist. So gesehen ist der einfachste Assert:

import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class SimpleTest {

   @Test
   public void test() {
      assertTrue("Das weiß doch jedes Kind.", 1+1 == 2);
   }

}

Der Methode assertTrue wird ein logischer Ausdruck übergeben, der im tatsächlichen Testfall sicherlich keine Tautologie wie in meinem Fall sein wird und diese wird geprüft.
Sämtlichen assert-Methoden kann (und sollte!) eine Fehlermeldung übergeben werden, die im Falle eines (erwarteten) Fehlers ausgegeben wird und damit die Fehlersuche erleichtern soll.
Das mag einem in obigem Beispiel nicht so einleuchten, aber in komplexeren, größeren assert-Blöcken wird das Fehlschlagen eines einzelnen Tests im Build-Durchlauf eines CI-Servers wie Jenkins mit dem Hinweis: java.lang.AssertionError zu einer Nadelsuche im Nadelhaufen.

Um die Assertions insgesamt lesbarer zu gestalten, wurde die assertThat Methodengruppe eingeführt:

...
   @Test
   public void test() {
      assertThat(myObj, is(not(nullValue()));
   }
...
}

Würde man die Prüfung, dass ein Objekt nicht null ist auf Englisch übersetzen, stünde dort:

Assert that myObj is not a null value.

Man beachte, dass is, not und nullValue drei (statische) Methoden einer Matchers-Klasse sind.

Hamcrest

Das Framework Hamcrest entstand, weil viele Entwickler unzufrieden mit den bereits bestehenden assertEquals Methoden waren.

Die assertEquals – Methode erwartet als ersten Parameter den erwarteten Wert und als zweiten Parameter den tatsächlichen Wert. Ein Umstand, der mich persönlich immer für einige Sekunden blockierte, bis mir klar war, was ich wo übergeben musste. Ich würde es damit vergleichen, etwas in Spiegelschrift zu lesen. Es geht, aber es ginge leichter.

Mit der Einführung der assertThat – Methoden (und seit Version 4.7 ist ein Teil davon in JUnit integriert) konnte man nun schreiben:

   assertThat("Da stimmt was nicht...", meinZuPruefenderWert, is(erwarteterWert));

Was hier genau passiert, erfahren wir durch einen Blick in den Code:

...
public static <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
    if (!matcher.matches(actual)) {
        Description description = new StringDescription();
        description.appendText(reason)
                   .appendText("\nExpected: ")
                   .appendDescriptionOf(matcher)
                   .appendText("\n     but: ");
        matcher.describeMismatch(actual, description);
        
        throw new AssertionError(description.toString());
    }
}
...

Der erste Parameter ist ein Fehlertext, der im Fehlerfall mitausgegeben werden soll. Dies sieht man in Zeile 6.

Der zweite Parameter ist der tatsächliche Wert (das zu prüfende Objekt) und wegen der Flexibilität ein generischer Typ T.

Der dritte Parameter hängt nun typmäßig vom zu vergleichenden Objekttyp ab und stellt einen Matcher dar. Man könnte Matcher als eine spezielle Implementierung der Vergleichslogik sehen. Vielleicht ähneln sie ein wenig den java.util.Comparator Klassen. Der Matcher legt fest, wann eine Übereinstimmung (Match) vorliegt.

Hierzu zwingt einen das Interface Matcher, dass man zwei Methoden implementiert:

  • boolean matches(java.lang.Object)
    Hierin wird die Logik implementiert, wann zwei Objekte übereinstimmen, also ein Match vorliegt. Das kann im Falle des Is-Matchers eine equals-Logik sein; im Falle des Not-Matchers eine logische Negation davon.
  • void describeTo(Description)
    Diese Methode gibt an, wie der Matcher bezeichnet werden möchte.
    Beispielsweise beim Is-Matcher steht „Expected: is but: was“.

Natürlich sind mehrere Matcher-Kombinationen gleichwertig und können beliebig benutzt werden. Das „Zuckerstückchen“ wird von Hamcrest eben geliefert, um die Lesbarkeit der Tests zu erhöhen.

HAMCREST (was man mit Schinkenhügel übersetzen könnte) ist übrigens ein Anagramm des Wortes MATCHERS.

Prinzipiell gilt bei Assertions, dass, falls eine Assertion fehlschlägt, alle weiteren NICHT mehr geprüft werden, was in den meisten Fällen dazu führt, dass Tests eher klein sind und dafür viele, um im Falle eines Fehlers die Suche nach der Lösung zu erleichtern. Dies sollte man beim Refacotring in TDD unbedingt beachten.

Verifications

Verifications dienen dazu, zu überprüfen, ob eine bestimmte Methode eines bestimmten Objekts aufgerufen wurde und, falls ja, wie oft. Dies kann jedoch nur auf ge“mock“ten Objekte passieren, welche alle verify-Methoden Teil des Mocking-Frameworks Mockito sind.

Eine kleine Anmerkung am Rande: Natürlich gibt es auch andere Mocking-Frameworks wie beispielsweise jMock. Da ich mit diesen jedoch noch nicht gearbeitet habe, konzentriere ich mich hier auf Mockito.

import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class SimpleTest {

   @Mock
   private MyService myService;

   @InjectMocks
   private Simple simple;

   @Test
   public void testRightServiceCall() {
      simple.doSimpleThings();
      verify(myService).doSomethingImportant(3);
   }

   @Test
   public void testWrongServiceCall() {
      simple.doSimpleThings();
      verify(myService, never()).doSomethingImportant(5);
   }

   @Test
   public void testWrongNumberOfServiceCall() {
      simple.doSimpleThings();
      verify(myService, times(2)).doSomethingImportant(3);
   }

}

Die ersten beiden Tests laufen grün, der dritte Test schlägt mit folgender Fehlermeldung fehl:

org.mockito.exceptions.verification.TooLittleActualInvocations: 
myService.doSomethingImportant(3);
Wanted 2 times:
-&gt; at de.mike.tests.SimpleTest.testWrongNumberOfServiceCall(SimpleTest.java:37)
But was 1 time:
-&gt; at de.mike.tests.Simple.doSimpleThings(Simple.java:8)

	at de.mike.tests.SimpleTest.testWrongNumberOfServiceCall(SimpleTest.java:37)
        ...

Wie man sehen kann, stimmt die angegebene Anzahl der zu verfizierenden Aufrufe nicht überein („Wanted 2 times but was 1 time.“)

Um zu verstehen, was in dem obigen Test genau passiert, müsste ich mehr auf Mockito eingehen, was ich hier bereits getan habe.