
Der Lügner – Immer schön den Schein wahren! Bild abgewandelt von „Poker“ by Steven Lilley, http://flic.kr/p/dZeNvG CC SA BY 2.0
Immer schön den Schein wahren!
Beim Lügner laufen alle Testfälle erfolgreich durch. Eigentlich ganz schön, oder? Bei genauerer Betrachtung fällt ins Auge, dass nicht einer der Testfälle das erwartete Verhalten prüft.
Das Problem
Ein guter Testfall sollte immer genau einen Aspekt, ein gewünschtes Verhalten prüfen. Der Name oder die Beschreibung des Testfalls gibt an, was getestet wird. Der Lügner behauptet hierbei, ein bestimmtes Verhalten des Object under Test (OUT) zu verifizieren, testet jedoch daran vorbei. Es werden unwichtige Randfälle geprüft, oftmals lediglich die vorgegebenen Eingaben verifiziert oder einfach die Objektinstanziierung mit einem Test auf not null
abgesichert.
Das Problem hierbei ist, dass eine Testsuite stets die Spezifikation des OUT darstellen sollte. Findet sich dort ein Testfall, der auf ein bestimmtes Verhalten hinweist, muss man davon ausgehen, dass das entsprechende Verhalten implementiert wurde und funktioniert. Andernfalls wird die Weiterentwicklung und Fehlersuche unnötig aufwändig und verkompliziert. Zusätzlich ist der entsprechende Testfall wertlos.
Ein Beispiel
Im folgenden Beispiel wird behauptet, dass der Testfall carHasFourTires()
prüft, ob ein Objekt vom Typ Car
vier Reifen vom Typ Tire
hat. Beim genaueren Hinsehen fällt auf, dass die Prüfung auf Räder vom Typ Wheel
erfolgt und nicht auf die Reifen vom Typ Tire
selbst. Erst beim Durchlesen des Kommentars oder – schlimmer noch – bei der späteren Fehlersuche würde auffallen, dass hier auf den falschen Typ geprüft wurde.
public class CarTest { /** * Every car is supposed to have four tires of * type <code>Tire</code>. */ @Test public void carHasFourTires() { Car car = new Car(); assertNotNull(car); assertNotNull(car.getChassis()); assertNotNull(car.getChassis().getWheels()); assertEquals(4, car.getChassis(). getWheels().size()); } }
Die Indizien
- Name und Implementierung des Testfalls passen nicht zusammen
- Testfälle mit Prüfungen auf
not null
odernull
- Testfälle mit auskommentierten Assertions
- Fehlersuche aufwändig
Die Ursachen
In der Regel kommt der Lügner zustande, wenn man sich vornimmt, ein bestimmtes Verhalten zu testen, beim Schreiben des Testfalls aber merkt, dass das Test-Setup aufwändig ausfallen würde. So entschließt man sich, wenigstens die erfolgreiche Objektinstanziierung zu prüfen und das für ausreichend zu befinden. Ein Test auf korrekte Objektinstanziierung mittels not null
ist in den meisten Fällen jedoch nicht mal notwendig – geschweigend denn hinreichend, da diese implizit mitgetestet wird. Im obigen Beispiel ist etwa wenigstens die Prüfung, ob das Car
-Objekt instanziiert wurde, überflüssig und steht dem Prinzip des hierarchischen Testens bei Modultests entgegen.
Manchmal kann es auch an dem Design des OUT liegen, dass ein zu testender Aspekt kompliziert nachzustellen oder zu überprüfen ist. Entweder testet der Testfall einen internen Aspekt, der nicht explizit über die Schnittstelle des OUT zu prüfen ist. Oder die Schnittstelle ist nicht korrekt entworfen, so dass das fachlich interessante Verhalten nicht überprüfbar ist.
Ähnlich verhält es sich, wenn man schlichtweg das falsche OUT für den zu prüfenden Aspekt wählt. Im obigen Beispiel wäre es denkbar, dass versucht wird, das Vorhandensein der vier Tire
-Objekte an der falschen Stelle zu testen.
Gelegentlich erwischt man sich auch dabei, sich beim Schreiben eines Testfalls an der Implementierung entlangzuhangeln, um am Ende zu vergessen, was man eigentlich testen wollte. Dies geschieht insbesondere, wenn der Testfall schlecht benannt wurde oder man sich keinen konkreten Aspekt des Verhaltens ausgesucht hat, den man testen möchte.
Die Lösung
Vorbeugen
Um gar nicht erst einen Lügner hervorzubringen, können folgende Richtlinien helfen.
Konsequentes TDD
Die beste Möglichkeit dem Lügner vorzubeugen, liegt darin, konsequent mit TDD zu entwickeln. Dabei beugt man durch die iterative Vorgehensweise – nur einen Aspekt zur Zeit zu implementieren – und den inhärenten Refaktorierungsschritt den meisten genannten Problemen vor.
Testfall und Assertions abgleichen
Des Weiteren sollte man bei jedem geschriebenen Testfall genau prüfen, ob man das postulierte Verhalten wirklich prüft, d.h., ob der Name des Testfalls und die enthaltenen Assertions übereinstimmen.
So wäre im obigen Beispiel carHasFourWheels()
ein passenderer Name für den Testfall.
An der richtigen Stelle testen
Für jedes zu testende Verhalten muss man sich fragen, welche Klasse das Verhalten beheimatet oder beheimaten sollte. In Konsequenz kann das auch eine Refaktorierung der Implementierung nach sich ziehen, wenn dabei auffällt, dass das Verhalten fachlich an der falschen Stelle untergebracht wurde.
So stellt im obigen Beispiel die Klasse Chassis
eventuell einen besseren Einstiegspunkt für den Testfall dar.
Nacharbeiten
Hat man den Lügner enttarnt oder vermutet man einen flunkernden Testfall, gibt es eine Reihe von Techniken, ihm beizukommen.
Überflüssige Prüfungen entfernen
Auch gilt es, bei jeder Prüfung die Frage zu stellen, ob die Assertion etwas mit dem Testfall zu tun hat. Dabei werden bereits getestete Aspekte, Module, verwendete Frameworks und insbesondere die Sprache selbst als funktionsfähig angesehen und nicht mitgetestet. Entfernt man nach und nach die überflüssigen Prüfungen, wird oft schnell klar, ob und welche Prüfungen noch fehlen, um den postulierten Testfall abzudecken.
Testfälle aufteilen
Ähnlich kann man vorgehen, indem man für jede Assertion prüft, ob sie nicht einen eigenen Testfall darstellt. Diese Prüfungen werden in separate Testfälle ausgelagert.
Im Beispiel carHasFourTires()
könnte man den Testfall in carHasFourWheels
umbenennen und in der Klasse Wheel
zu prüfen, ob jedes Wheel
-Objekt ein Tire
-Objekt besitzt.
Auch bekannt als
- The Liar
Siehe dazu auch James Carrs fantastischen Blogbeitrag zu TDD-Anti-Patterns und die dazugehörige Diskussion auf der Test-Driven Development Yahoo Group.