Massentest mit JUnit 5

Immer wieder kommt es vor, dass Softwareentwickler ihren alten Quellcode bearbeiten müssen. Dabei ist der Entwickler mit dem Werk der letzten sechs Wochen immer recht zufrieden. Dinge, die noch nicht älter als sechs Monate sind, werden mit einer oberlehrerhaften Skepsis betrachtet und im Kopf werden eifrig Änderungen notiert, die das frühere Ich vergessen hat. Was älter ist als sechs Monate, wird behandelt, als wäre es von fremder Hand erstellt. Zum einen, weil viele Codeänderungen schon längst vergessen sind, oder weil man nicht akzeptieren will, diesen Schund selbst geschrieben zu haben.

Bei der Erstellung von Unit Test sollte darauf geachtet werden, dass sie knapp, präzise und selbsterklärend sind. Knapp soll der Unit Test in seiner Formulierung sein und nur das absolut Notwendige für den Testfall bereitstellen. Die Prüfung des Ergebnis muss präzise sein damit der Unit Test tatsächlich fehlschlägt, wenn sich ein Fehler in die Anwendung eingeschlichen hat. Außerdem muss der Unit Test selbsterklärend sein, damit der Entwicklung ein schnelles Verständnis des Testfalls bekommt, wenn dieser tatsächlich einmal fehlschlägt. Da dies auch nach Monaten geschehen kann, sollte ein Unit Test kein Buch mit sieben Siegeln für den Entwickler sein..

Das folgende Beispiel zeigt einen Unit Test, der den oben genannten Anforderungen nicht mehr entspricht. Aus Unachtsamkeit wurde der ursprüngliche Test um weitere Testfälle ergänzt. In diesem Fall tummeln sich nun drei Testfälle in der Test Methode. Es können aber auch weitaus mehr sein Testfälle in einer Test Methode versteckt sein.

@Test
void findByName() {
  System.out.println("findByName Jens /Kaiser/");
  Person person = ancestors.findByName("Jens /Kaiser/");
  assertEquals(1L, person.getId());
  assertEquals(LocalDate.of(1968, 8, 24), person.getBirth().getDate());

  System.out.println("findByName Hermann /Seemann/");
  person = ancestors.findByName("Hermann /Seemann/");
  assertEquals(23L, person.getId());
  assertEquals(LocalDate.of(1815, 7, 17), person.getBirth().getDate());

  System.out.println("findByName Johann /Seemann/");
  person = ancestors.findByName("Johann /Seemann/");
  assertEquals(42L, person.getId());
  assertEquals(LocalDate.of(1781, 9, 6), person.getBirth().getDate());  
}

Tritt ein Fehler in dieser Test Methode auf, dann ist nicht sofort klar, warum der Test fehlgeschlagen ist. Ist ein grundsätzlicher Fehler in der findByName Methode zu vermuten oder haben sich, bei einem der drei Person Instanzen, irgendwelche Daten geändert?

Die Ausgaben mit System.out.println sind eine Notlösung des Entwicklers gewesen um ausmachen zu können, wie viel von diesem Testfällen durchlaufen wurden.

Um diesen Test Smell zu entfernen, kann der Unit Test in drei einzelne Unit Tests aufgesplittet werden. Da aber alle drei Testfälle strukturell identisch sind, bieten sich hier parametrisierte Tests an.

In JUnit 5 wird die Test Methode dazu mit der Annotation @ParameterizedTest, anstelle der üblichen Annotation @Test versehen. Außerdem muss eine Quelle für die Test Parameter per Annotation definiert werden. Junit 5 bietet dafür diverse Möglichkeiten, u.a. die @CsvSource Annotation, die im folgenden Beispiel verwendet wird.

@ParameterizedTest(name = "{index} => {2}")
@CsvSource({
    " 1, 1968-08-24, Jens /Kaiser/",
    "23, 1815-07-17, Hermann /Seemann/",
    "42, 1781-09-06, Johann /Seemann/",
})
void findByName(Long expectedId, LocalDate expectedDate, String name) {
  Person person = ancestors.findByName(name);
  assertAll(
    () -> assertEquals(expectedId, person.getId());
    () -> assertEquals(expectedDate, person.getBirth().getDate()));
}

Der erste wichtige Unterschied zur vorherigen Test Methode sind die Parameter expectedId, expectedDate und name. Die Parameter werden aus den Strings befüllt, die in der @CsvSource Annotation definiert sind. Jeder String enthält kommasepariert die Parameter für einen Testfall. Die Konvertierung in die notwendigen Typen geschieht, für eine Reihe von Klassen, automatisch. Bei exotischeren Testwerten ist leider Handarbeit angesagt.

Da nun die drei Testfälle einzeln bearbeitet werden, wird auch ihr Erfolg oder Misserfolg separat protokolliert. Ein Fehler kann so schneller eingegrenzt werden.

Um den Name der einzelnen Testfälle zu modifizieren, kann das name Attribute der @ParameterizedTest Annotation verwendet werden. In diesem Fall wird der Index des Testfalls und der Name der Person ausgegeben. Ohne dieses Attribut werden alle Parameter ausgegeben.

Ein weiterer Vorteil der @CsvSource Annotation ist die Darstellung der Parametersätze direkt an der Testmethode. Bei Fehlern in dem parametrisierten Test, kann der Fehler schneller gefunden werden, weil die Test Parameter nicht erst herausgesucht werden müssen.