Ein Programmierer sieht rot: Was tun bei fehlerhaften JUnit-Tests?

Smoke-Test

Warum heißt der JUnit-Runner von PatternTesting eigentlich SmokeRunner? Ursprünglich war er dazu auserkoren, nur ausgewählte „Smoke“-Tests zu starten, um schnell einen Überblick über den Zustand des Projekts zu bekommen:

@SmokeTest
@Test
public final void testCryptString() {
    String s = Rot13.crypt("Hello");
    assertEquals("Uryyb", s);
}
Smoke-Test

Der Begriff „Smoke-Test“ kommt ursprünglich aus den Anfängen der PC-Geschichte. Damals konnte man beim Anschließen des Netzteils an die Hauptplatine leicht die Polung verwechseln. Ein erster Test bestand damals darin, das Netzteil einzuschalten und zu schauen, ob Rauch aufstieg. Falls nicht, war der „Smoke-Test“ bestanden und man konnte sich an die weitere Arbeit machen.

Wenn einer dieser Tests, die als @SmokeTest gekennzeichnet sind, fehlschlägt, machen die restlichen Tests keinen Sinn. Gesteuert werden diese Tests über die Property patterntesting.runSmokeTests – ist sie gesetzt, werden nur diese Tests ausgeführt (Kasten: „Smoke-Test“).

Tauscht man den SmokeRunner durch den ParallelRunner aus, werden die Testmethoden sogar parallel ausgeführt. Allerdings ist die Verkürzung der Durchlaufzeit eher gering, da die Testmethoden meistens zu kurz für eine spürbare Beschleunigung sind. Aber bei mehreren längeren Methoden (die ja eigentlich schon Integrationstests sind), kann sich die Parallelität auszahlen – einen Versuch ist es jedenfalls wert. Der ParallelRunner ist vom SmokeRunner abgeleitet und nutzt dabei die gleichen Annotationen. Allerdings müssen die Testmethoden wirklich unabhängig voneinander sein. Vor allem Singletons können sich hier als Problemfälle für Multithreading herausstellen.

Weitere Testunterstützung

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes. (aus den Javadocs zur Object.equals-Methode)

Wenn man die equals()-Methode überschreibt, sollte man die hashCode()-Methode nicht vergessen. Zwei gleiche Objekte müssen auch den gleichen Hashcode zurückliefern (nicht umgekehrt). Sonst funktionieren z. B. die HashMap oder andere Collections-Klassen, die auf dem Hashcode aufbauen, nicht mehr richtig. Trotzdem wird dies von vielen Programmierern (vor allem Anfängern) gerne vergessen. Deswegen bietet PatternTesting die Möglichkeit, solche „Kleinigkeiten“ automatisert zu testen:

ObjectTester.assertEquals(Rot13.class);

Damit wird die equals()-und hashCode()-Methode der Rot13-Klasse geprüft, ob sie sich tatsächlich an die Vorgabe aus den Javadocs halten. Vorraussetzung dafür ist, dass die Klasse einen Default-Konstruktor bereitstellt, der jedes Mal ein gleiches Objekt erzeugt (das ist nicht immer der Fall, wie z. B. bei der Date-Klasse) oder die Klasse das Cloneable-Interface unterstützt (dies gilt z. B. für die Date-Klasse), damit PatternTesting zwei „gleiche“ Objekte erzeugen kann. Alternativ kann man auch zwei gleiche Objekte oder auch ein ganzes Package für den Massentest an die checkEquals()-Methode übergeben:

ObjectTester.assertEquals(x, y);
ObjectTester.assertEquals(this.getClass().getPackage());

Weitere Problemfälle können durch folgende assert-Methoden aufgespürt werden:

  • ObjectTester.assertCompareTo(..):Wenn zwei Objekte einer Comparable-Klasse gleich sind, muss die implementierte compareTo()-Methode „0“ zurückliefern.
  • ObjectTester.assertToString(..):Ein ärgerlicher Fehler ist, wenn ein frisch kreiertes Objekt bei der Ausgabe mit einer NullPointerException aussteigt, nur weil die toString()-Methode schlampig implementiert wurde.
  • ObjectTester.assertAll(..):Es führt alle Überprüfungen aus, die der ObjectTester anbietet.
  • SerializableTester.assertSerialization(..):Klassen, die serialisierbar sind, sollten keine NotSerializableException werfen, wenn sie tatsächlich mal serialisiert werden. Oft wird das aber nicht getestet (ist auch gar nicht so einfach) und oft wird das Problem auch nicht gesehen, weil z. B. Sessions bei Webanwendungen fast nie serialisiert werden – zumindest nicht beim Testen.
  • CloneableTester.assertCloning(..):Beim Clonen sollte keine Exception auftreten und Original und Clone sollten natürlich gleich sein.

Wird als Parameter ein Package angegeben, werden nur die Implementierungen überprüft, die auch tatsächlich überschrieben wurden. Auch kann man mit Exclude-Listen arbeiten, um bekannte Problemfälle vom Test ausschließen zu können.

Einbindung

Auch wenn PatternTesting eine AspectJ-Bibliothek [2] ist, kann sie ganz normal als Java-Bibliothek eingebunden werden. Für die Testunterstützung reicht dabei die Runtime-Komponente (PatternTesting Runtime), die in der zum Download bereitgestellten patterntesting-libs.zip enthalten ist. Für Maven reicht folgende Angabe in den Abhängigkeiten:

org.patterntestingpatterntesting-rt1.1.0
Fazit

Einen roten Balken wieder auf grün zu bekommen, kann manchmal ganz schön nervig sein. Vor allem die Menge der fehlgeschlagenen Tests bereitet Kopfzerbrechen. Und es gehört in manchen Projekten eine gute Portion Durchhaltevermögen dazu, bis man wieder im grünen Bereich ist. Aber es lohnt sich. Mit ein paar kleinen Tricks wie das Umbenennen von Testmethoden (JUnit-3) oder dem Einsatz der PatternTesting-Bibliothek kommt man diesem Ziel schrittweise näher. Solange, bis es heißt: „Ein Programmierer sieht grün!“

Denken Sie immer daran: Tests sind die Visitenkarte eines Projekts und die Grundlage agiler Vorgehensweise.

Oliver Böhm ist seit einigen Jahren Committer bei PatternTesting und hat inzwischen viele Erfahrungen aus diversen Projekten in dieses Projekt einfließen lassen können. Außerdem ist er Autor mehrerer Bücher (u. a. „Aspektorientierte Programmierung mit AspectJ 5“).
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.