Qualitätssicherung ausgewogen

Seite 2: Testautomatisierung

Inhaltsverzeichnis

Testautomatisierung ist immer noch keine Praktik, die ausreichend weit verbreitet ist. Hintergrund dafür sind vermutlich schlechte Erfahrungen mit automatisierten Tests. Gerard Meszaros listet in seinem Buch "xUnit Test Patterns – Refactoring Test Code" [2] einige sogenannte Test Smells auf, die Probleme mit schlecht automatisierten Tests verdeutlichen.

Fragile Tests haben dabei vielerlei Ursachen. Zum einen können automatisierte Tests zu stark an die getesteten Klassen gebunden sein. Zum anderen sind zu viele Abhängigkeiten beispielsweise zum Dateisystem oder zu Drittsystemen wie der Datenbank ein häufiges Problem. Tests werden dadurch nicht nur langsam, sondern sind auch an die spezifischen Implementierungsdetails der Datenbank oder der Dateisystemstruktur gebunden. In einigen Fällen verhindern sie sogar notwendige Umstrukturierungen des Codes, wie bei Refactorings, statt diese mit konstruktivem Feedback zu unterstützen.

In der Praxis zeigt sich außerdem, dass Tests lesbar und ihre Ergebnisse eindeutig sein müssen. Ein Unit-Test, bei dem mehrere Minuten nachzuforschen ist, warum er nicht durchlaufen konnte, kann bei einer Testsuite von 2000 Testfällen schnell zu einem Problem werden. Schlagen dann noch mehrere Unit-Tests auf einmal fehl, ist häufig vergleichsweise viel Zeit in das Finden der Ursachen zu stecken. Tests, die ein einfaches Feedback darüber liefern, was an einer Software kaputt gegangen ist, helfen hingegen dabei, Fehler schnell zu beheben. Werden sie stets lauffähig gehalten, liegt auch kontinuierlich eine funktionierende Softwareversion vor, wodurch stets aktuelle Software lieferbar ist.

Ein Muster für gute Testautomatisierung ist das Arrange-Act-Assert-Muster, wobei ein Test in drei bis vier Phasen aufgeteilt wird. In der ersten Phase, dem Arrange, sind alle Vorbereitungen für den Test zu treffen: Der Entwickler baut die Testfixture zusammen, instanziiert alle abhängigen Objekte und bereitet den Test insgesamt vor. Danach folgt der Act-Teil des Tests, in dem es darum geht, eine Aktion auf dem zu testenden Objekt auszuführen. Dabei ist es wichtig, dass es sich um eine einzelne Aktion handeln sollte. Sonst hat der Test wieder mehrere Stellen, an denen er fehlschlagen kann, und ist im Fehlerfall aufwendiger zu analysieren. In der dritten Phase, Assert, helfen Assertions sicherzustellen, dass alle Nachbedingungen einer Funktion eingetroffen sind. In der Regel sollte ein guter Unit-Test nicht mehr als eine Assertion verwenden. Hin und wieder findet außerdem noch eine vierte Phase Verwendung: der TearDown. Hier müssen einige persistente Objekte nach dem Testlauf wieder aufgeräumt werden. Häufig ist das bei funktionalen Tests der Fall, die mehr als ein Objekt auf einmal heranziehen. Das kann beispielsweise bei Akzeptanztests der Fall sein oder bei Tests, die mehrere Schichten der Applikation durchlaufen. In dieser Phase ist durch das Wiederherstellen des Zustands, wie er vor dem Test war, dafür zu sorgen, dass alle folgenden Durchgänge fehlerfrei laufen können.

In der Act-Phase lässt sich zudem zwischen sogenannten Query- und Command-Methoden unterscheiden. Idealerweise sollte eine Funktion nur eines von beiden tun: entweder einen Wert zurückliefern oder das Objekt intern verändern. Bei den Query-Methoden speichert der Test den zurückgelieferten Wert in einer Variablen und prüft ihr gegenüber die Nachbedingungen. Bei einer Command-Methode verändert er den internen Zustand des Objekts und muss anschließend – gegebenenfalls unter Ausnutzung vorhandener Query-Methoden – die Nachbedingungen überprüfen.

Nicht zuletzt sind gute Unit-Tests kurz: Idealerweise sollten sie 20 Zeilen nicht übersteigen. So bleiben Tests auf einen Teilaspekt der Applikation fokussiert und im Fehlerfall lässt sich der Wirkungsort schnell feststellen (Abb. 1). In den meisten Fällen, in denen eine dieser Regeln – eine Assertion pro Testmethode, mehr als 20 Zeilen – verletzt wird, deutete der schlechte Test auf eine Designschwäche hin.

Ein Test sollte immer nur eine mögliche Fehlerursache prüfen und nur bei dieser fehlschlagen (Abb.1).


Meistens dauert es auch nicht lange, diese Schwäche zu entdecken und sie zu beheben. Die Unit-Tests sind hier ein Spiegel für die gute oder schlechte Qualität der Schnittstellen. Der erste Nutzer einer Klasse findet sich stets in den Unit-Tests, und wenn diese unfokussiert erscheinen, also viele offenbar unbeteiligte Objekte heranziehen müssen, deutet das darauf hin, dass auch der Produktivcode unfokussiert wird, sollte er die neue Klasse verwenden.

Testautomatisierung allein ist bereits ein wichtiger Schritt in Richtung Softwarequalität. Langfristig wollen Entwickler jedoch häufig mit kontinuierlichem Feedback zu aktuellen Änderungen im Code arbeiten. Dies funktioniert im Team nur durch das regelmäßige Integrieren eigener Änderungen mit denen der Kollegen (Abb. 2). Continuous-Integration-Systems (CI) liefern dabei nicht nur Feedback darüber, ob die Software gebaut werden kann, sondern sind auch in der Lage, viele lästige Tätigkeiten zu übernehmen.

Mit Continuous Integration soll sich unter anderem ein Merge-Chaos kurz vor dem Ausliefern vermeiden lassen (Abb. 2).

Neben dem Bauen von Applikationen können heutige CI-Systeme durch zusätzliche Plug-ins Metriken und Analysen ausführen. Ein Mindestmaß dabei ist, dass die Applikation gebaut werden können sollte und alle Unit-Tests ausgeführt werden. Teams, die schon firmer in Continuous Integration sind, setzen zudem auf Code-Abdeckung durch die Unit-Tests sowie statische Code-Analyse und lassen gegebenenfalls Artefakte nach einem erfolgreichen Build vom CI-System erzeugen und publizieren.

Für die Code-Abdeckung ist weniger das prozentuale Ziel sinnvoll, sondern die kontinuierliche Verbesserung. Allgemein unterscheidet man zwischen Line- und Branch-Coverage. Darüber hinaus gibt es die Klassen- und Methoden-Abdeckung, die in der Praxis aber häufig als weniger aussagekräftig angesehen wird.

Bei der Line-Coverage handelt es sich um den Prozentsatz an Codezeilen, den die Unit-Tests abdecken. Die Branch-Coverage hingegen beschreibt die Abdeckung von Zweigen, die im Ausführungspfad der Anwendung möglich sind. Hierbei wird darauf geachtet, dass bei jeder Bedingung im Code jeder alternative Pfad ebenfalls mit abgedeckt wird.

Eine Falle, in die viele Teams mit Coverage-Metriken laufen, ist, dass die Metrik selbst nichts darüber aussagt, ob für das Ergebnis einer Zeile Code auch eine entsprechende Assertion auftaucht. Zwar lassen sich durch Mutationen in den Unit-Tests deren Güte bewerten, allerdings sind derartige Messungen aufwendig, da das gesamte Testset mehrfach zu durchlaufen ist. Beim Mutationstesten werden einzelne Werte im Unit-Test, beziehungsweise dessen Assertions, verändert. Funktioniert der Test weiterhin, handelt es sich um ein sogenanntes False Positive – der Test läuft durch, obwohl er fehlschlagen müsste. Scheitert der Test jedoch, fängt er genau den Fehler ab, den er finden soll.