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

PatternTesting

Wenn man viele Test-Methoden auf diese Art und Weise als „derzeit kaputt“ gekennzeichnet hat, besteht die große Gefahr, dass man sie nicht mehr wahrnimmt und die deaktivierten Tests langsam in Vergessenheit geraten, auch wenn man sich fest vorgenommen hat, den einen oder anderen Test später nochmals genauer anzuschauen. Sie machen sich ja auch so gut wie nicht bemerkbar und verschwinden langsam aus dem Blickfeld. Hier kommt jetzt PatternTesting [3] ins Spiel. PatternTesting besteht aus mehreren Bibliotheken, wobei PatternTesting Runtime eine Annotation @Broken anbietet, mit der man defekte Tests kennzeichnen kann. Ähnlich wie bei @Ignore werden die Tests zwar aufgelistet, aber nicht ausgeführt. Im Gegensatz zu @Ignore gibt es aber eine Reihe von Parametern, mit der man die kaputten Tests steuern kann, u. a.:

  • till = „01-Jan-2012“: Damit wird der Test bis zum 1.1.2012 als broken markiert. Danach wird der Test wieder ausgeführt und lässt den Testbalken wieder rot werden, wenn er bis dahin nicht repariert wird.
  • since = „13-Apr-2011“: Das ist das Gegenstück zum till-Parameter und dient nur zur Dokumentation. Damit kann man festhalten, seit wann der Test fehlschlägt.
  • why = „Umstieg auf JDK 1.7“: Zeigt an, warum der Test nicht mehr funktioniert. Diese Meldung erscheint auch im Log.

PatternTesting ist eigentlich eine AOP-Bibliothek für AspectJ, die aus mehreren (Teil-)Bibliotheken besteht und deren Ziel es ist, dem Entwickler beim Aufspüren von Anti-Pattern bzw. „Bad Practices“ im Java-Code zu unterstützen. Die Runtime-Komponente lässt sich aber auch als reine Java-Bibliothek einsetzen und bringt u. a. einen eigenen JUnit Runner mit, der sich mit @RunWith(SmokeRunner.class) für die Tests einbinden lässt:

@RunWith(SmokeRunner.class)
public class Rot13Test {
    ... 
}
Daily Tests Failed

Manchmal steht man vor dem Problem, dass der Test zwar lokal auf dem eigenen Rechner läuft, aber auf dem Build-Server fehlschlägt. Wenn man sich die testCryptBillsFile()-Methode nochmal genauer anschaut, wird man feststellen, dass dieser Test vermutlich nur unter Windows laufen wird, nicht aber auf dem Build-Rechner unter Linux. Daher stellt PatternTesting die Annotationen @SkipTestOn und @RunTestOn zur weiteren Steuerungsmöglichkeit zur Verfügung:

@RunTestOn(osName = "Windows")
@Test
public final void testCryptBillsFile() throws IOException {
    ... 
}

Dieser Test wird nur unter Windows ausgeführt werden. Neben osName können hier eine Reihe von weiteren Bedingungen angeben wie z. B.:

  • osArch = „x86_64“: Damit wird der Test nur auf einer 64-bittigen Intel-Plattform ausgeführt
  • host = „gatewaytoworld“: Damit wird der Test nur auf dem Rechner mit dem Namen „gatewaytoworld“ (alternativ kann auch eine IP-Adresse angegeben werden) gestartet
  • property = „DB-TEST“: Dieser Test wird nur ausgeführt, wenn die System-Property „DB-TEST“ gesetzt ist.

Es gibt noch weitere Bedingungen, die man angeben kann. Bei mehreren Parametern müssen alle Bedingungen erfüllt sein, damit der Test ausgeführt wird. @SkipTestOn ist das Gegenstück zu @RunTestOn und nutzt die gleichen Parameter.

Weitere Steuerungsmöglichkeit

Gerade die Unterstützung von Properties erlauben vielfältige Steuerungsmöglichkeiten. So kann man damit z. B. verschiedene Kategorien wie „DB-TEST“, „UI-TEST“ oder „LOAD-TEST“ definieren, die man beim Aufruf der Tests über Setzen von System-Properties ein- oder ausblenden kann („-DUI-TEST-DLOAD-TEST“).

Mutierte Unit Tests

Unit Tests müssen schnell gehen, sonst bremsen sie den fleißigen Entwickler zu sehr aus und erhöhen das Frustpotenzial. Leider gibt es in fast jedem Projekt immer einzelne Tests, die viel zu lange dauern. Schaut man sich diese Tests dann genauer an, stellt man fest, dass sie Daten aus der Datenbank holen, damit irgendwelche Berechnungen oder Manipulationen anstellen und evtl. sogar wieder zurück in die Datenbank schreiben – kurz, es ist in Wirklichkeit kein Unit Test mehr, sondern schon ein halber (oder ganzer) Integrationstest (Tabelle 1).

Tabelle 1: Was unterscheidet einen Unit Test von einem Integrationstest?

Unit Test Integrationstest
Überprüft, ob eine Methode das gewünschte Ergebnis liefert. Testet ein Teil- oder das Gesamtsystem und überprüft das Zusammenspiel mehrerer Klassen oder Komponenten.
Wird idealerweise nach jeder Codeänderung ausgeführt und darf nur wenige Millisekunden dauern. Dieser Test kann (und darf) länger dauern (bis zu mehreren Stunden) und wird weniger oft ausgeführt.

Es empfiehlt sich solche Tests, die eigentlich Integrationstests sind, in ein eigenes Integrationsprojekt auszulagern. Leider ist dies nicht immer (so schnell) möglich. Daher bietet PatternTesting auch die Möglichkeit, solche Tests als @IntegrationTest zu kennzeichnen:

@IntegrationTest("online access needed")
@Test
public final void testCryptURI() {
    ... 
}

Zusammen mit dem SmokeRunner von PatternTesting werden diese Tests komplett ignoriert – sie werden weder ausgeführt, noch erscheinen sie in der Liste der ausgeführten Testmethoden. Aber wie kann man dann diese Tests ausführen, wenn man sie braucht? Dies kann über die System-Property patterntesting.integrationTest gesteuert werden – bei gesetzter Property wird der Test ausgeführt (Abb. 3).

Abb. 3: System-Property zum Testen setzen
Kommentare

Schreibe einen Kommentar

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