Anti-Pattern-Kalender 2014 – Der Lügner

Testing-Anti-Pattern des Monats Januar

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

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 oder null
  • 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.

Holisticon AG — Teile diesen Artikel