Freiberufler, Java-Architekt und Java-Entwickler

Alles unter Kontrolle

Messung der Testabdeckung mit Open-Source-Tools

Unit-Tests sind vielleicht nicht jedermanns Liebling - aus dem Projektalltag sind sie jedoch kaum noch wegzudenken. Schließlich können Software-Entwickler damit ihren Programmcode gründlich testen und somit Fehler schon früh im Projektverlauf entdecken. Trotzdem bedeutet eine große Anzahl von Unit-Tests nicht automatisch, dass diese Tests auch alle kritischen Code-Stellen überprüfen. Um herauszufinden, für welche Code-Abschnitte weitere Tests notwendig sind, bietet sich der Einsatz spezieller Tools an. Dieser Beitrag zeigt am Beispiel eines frei verfügbaren Tools, welche Aussagekraft die Testabdeckung hat und wie ein passendes Tool wertvolle Entscheidungshilfen geben kann.

Einführung

Unit-Tests helfen, den erstellten Programmcode schon während der Programmierung zu testen. Da Unit-Tests von den Entwicklern geschrieben werden, überprüfen die Entwickler somit ihren Code selbst auf Korrektheit, idealerweise, ohne ihn zuvor auf einem Testsystem ausrollen zu müssen.

Ein einheitliches Testkonzept für Unit-Tests gibt es eher selten. Vielmehr ist üblicherweise jeder Entwickler alleine dafür verantwortlich, passende Unit-Tests zu schreiben. In der Praxis werden Unit-Tests häufig erst nach dem eigentlichen Code geschrieben, und dann auch nur je nach verfügbarer Zeit. Zudem arbeiten oft mehrere Entwickler an einer Komponente; die Entwicklung einheitlicher Unit-Tests ist dann besonders schwierig – manche Code-Abschnitte werden mehrfach getestet, andere vielleicht gar nicht.

Für einen Projektleiter oder Architekten ist daher schwer zu entscheiden, wie verlässlich die vorhandenen Unit-Tests sind. Und auch das Projektmanagement hätte gerne eine Aussage darüber, welchen Wert diese Tests haben, deren Entwicklung zunächst viel Geld kostet, scheinbar ohne dass sie die Funktionalität des zu entwickelnden Systems voranbringen.

Somit stellt sich die Frage, wie sich die Qualität von Unit-Tests sicherstellen lässt und wie man die erreichte Qualität sowohl intern wie extern transparent kommunizieren kann. Eine mögliche Antwort darauf ist die Ermittlung der Test-Abdeckung. Daher wird im Folgenden beschrieben, was es mit der Test-Abdeckung auf sich hat und welchen Nutzen sie stiften, aber auch welche Probleme sie verursachen kann.

Was ist Test-Abdeckung?

Mit der Test-Abdeckung lässt sich messen, welche Bereiche des Programmcodes eines Systems beim Ausführen von Tests durchlaufen werden und welche nicht. Die Test-Abdeckung als Metrik gibt an, wie groß der Anteil des durchlaufenen Codes am gesamten Programmcode ist. Anhand der Testabdeckung lässt sich zudem anschaulich im Detail ermitteln, welche Code-Abschnitte aufgerufen worden sind. Da sich die Test-Abdeckung kaum von Hand ermitteln lässt, sind dazu automatisierte Tests nötig, also in der Regel Unit-Tests.

Die virtuelle Maschine von Java bietet mehrere Ansatzpunkte, um die Test-Abdeckung zu messen. Ein verbreiteter Ansatz manipuliert den vom Compiler erstellten Bytecode und fügt Anweisungen hinzu, die, wenn sie aufgerufen werden, die jeweilige Operation aufzeichnen. Manche Tools verwenden die Profiling-Schnittstelle, um über den Ablauf des Systems informiert zu werden. In jedem Fall werden nach dem Ende der Tests alle aufgezeichneten Vorgänge statistisch zusammengefasst.

Es lassen sich verschiedene Arten der Test-Abdeckung unterscheiden, von denen vor allem zwei Varianten verbreitet sind: die Messung der durchlaufenen Programmanweisungen sowie die Messung der durchlaufenen Alternativen bei Entscheidungszweigen (z.B. bei Programmsprüngen oder Schleifen). Bei ersterem wird untersucht, wie viele der Programmanweisungen eines Systems ausgeführt werden. Für eine bessere Anschaulichkeit lassen sich diese Anweisungen auf unterschiedlichen Ebenen zusammenfassen, z.B. auf Zeilen, Methoden, Klassen oder Paketen.

Bei der Messung der durchlaufenen Entscheidungszweige wird ermittelt, ob alle möglichen Alternativen durchlaufen worden sind. Bei einer if-Anweisung kann damit beispielsweise festgestellt werden, ob auch der else-Block aufgerufen wurde. Da bei einem einzigen Aufruf einer Methode selten alle Blöcke einer if-Anweisung durchlaufen werden, werden alle Aufrufe statistisch zusammengefasst. Es geht also nicht darum, ob die verschiedenen Alternativen während der Ausführung eines Tests durchlaufen wurden, sondern während der Ausführung aller Tests.

Welchen Nutzen hat die Test-Abdeckung?

Die Ermittlung der Test-Abdeckung hilft in einem Projekt vor allem zwei Zielgruppen: Entwicklern und Projektleitern. Entwickler können die Test-Abdeckung dazu nutzen, Lücken in ihren Unit-Tests zu finden und durch neue Tests zu schließen. Dazu reicht in der Regel keine einfache Metrik aus, sondern die Entwickler müssen sich im Detail anschauen, welche Abschnitte des Programmcodes noch nicht durchlaufen worden sind, um maßgeschneiderte neue Tests zu erstellen.

Die Messung der Test-Abdeckung hilft aber nicht nur, Unit-Tests zu erstellen, sondern auch, vorhandenen Programmcode besser zu verstehen. Wenn es beispielsweise schwer zu erkennen ist, welcher Programmcode durch den Aufruf einer Methode durchlaufen wird, hilft es, die Test-Abdeckung für genau einen Unit-Test zu starten, der eben diese Methode aufruft. Anschließend wird ersichtlich, welche Teile des Systems durch den Methodenaufruf betroffen waren.

Zudem macht die Messung der Test-Abdeckung die vorhandenen Tests transparenter. Sie bietet damit denjenigen Entwicklern, die Unit-Tests genau nehmen, die Möglichkeit, ihren eventuell höheren Entwicklungsaufwand damit zu rechtfertigen.

Projektleiter wiederum können anhand der Metrik der Test-Abdeckung überprüfbare Vorgaben über die Erstellung der Unit-Tests aufstellen. Die reine Anzahl von Tests alleine ist selten ein gutes Qualitätsmerkmal. Die Vorgabe hingegen, bestimmte Code-Bereiche zu einem bestimmten Prozentsatz abzudecken, lässt bessere Rückschlüsse über die erstellten Tests zu.

Wichtiger noch als der absolute Prozentsatz der Test-Abdeckung ist seine Varianz im Laufe der Zeit. Wenn die Metrik über einen längeren Zeitraum ermittelt und aufgezeichnet wird, lassen sich an ihr Trends ablesen. Beispielsweise, ob auch in Zeiten eines großen Projektdrucks weiterhin auf Unit-Tests geachtet wird, oder ob diese Tests schrittweise ausgeschaltet werden, zum Beispiel, weil die Zeit fehlt, Änderungen an der Code-Basis nachzuziehen. Andersherum lässt sich damit natürlich auch der Fortschritt in der Abdeckung des Codes durch Tests nachvollziehen, falls diese Tests nicht von Beginn an geschrieben, sondern erst später eingeführt worden sind.

Weiterhin können Projektleiter die Test-Abdeckung als Kommunikationsmittel gegenüber dem Management verwenden. Zuerst können Projektleiter damit die Notwendigkeit für mehr Zeit und Budget für Unit-Tests vermitteln; später können sie anhand des Verlaufs der Metrik den Erfolg der Maßnahmen dokumentieren.

Welche Probleme bringt die Messung der Test-Abdeckung mit sich?

Wie bei praktisch jedem Mittel zur automatisierten Bestimmung von Code-Qualität ist auch die Test-Abdeckung mit Vorsicht zu genießen. Es ist verführerisch, die Qualität der Tests auf eine einzige, objektiv messbare Zahl zu reduzieren, zumal sie sich für Management-Präsentationen über den Projektfortschritt anbietet. Leider sagt die Test-Abdeckung nur etwas darüber aus, wie viel Programmcode durch Tests durchlaufen wurde, nicht jedoch, wie sinnvoll diese Tests sind.

Beispielsweise mag es sinnvoll sein, generierten Programmcode von Unit-Tests auszunehmen. Oder aufgrund von Zeitdruck soll nur die Geschäftslogik getestet werden, nicht jedoch die Logik zur Steuerung der Benutzungsoberfläche. Zudem können Unit-Tests einen großen Bereich des Codes durchlaufen, ohne aber die Ergebnisse der einzelnen Aufrufe des Systems auf ihre fachliche Korrektheit hin zu prüfen. Die Unit-Tests haben in einem solchen Fall nur wenig Aussagekraft. Die Gefahr liegt also vor allem darin, die Ergebnisse der Test-Abdeckung als alleinigen Maßstab anzusehen.

Zudem nimmt ab einem gewissen Wert der Nutzen einer immer höheren Test-Abdeckung im Verhältnis zum Aufwand stark ab. Spätestens, wenn versucht wird, bei allen Vorkommnisse von if (log.isDebugEnabled()) log.debug(„...“); beide Verzweigungsmöglichkeiten durch Tests abzudecken, wird über das Ziel hinausgeschossen.

Die Transparenz, die durch die Ermittlung der Test-Abdeckung erreicht wird, kann bei manchen Entwicklern zudem ein ungutes Gefühl hinterlassen. Auch eine geringe Test-Abdeckung ist vielleicht gut genug, wenn alle wichtigen Bereiche gründlich getestet werden. Diese Entscheidungskompetenz wird durch eine Metrik wie die der Test-Abdeckung hinterfragt. Daher sollten keine persönlichen Ziele an die Test-Abdeckung gekoppelt werden.

Ein konkretes Beispiel

Im Java-Umfeld gibt es erfreulicherweise mehrere frei verfügbare Tools, mit denen sich die Test-Abdeckung ermitteln lässt. Jedes dieser Tools hat seine eigenen Stärken und Schwächen, sodass es sich lohnt, mehrere Tools auszuprobieren. Beispielhaft sei das Tool EclEmma herausgegriffen, dessen Stärke als Eclipse-Plug-In in der Integration in die Eclipse-Oberfläche liegt.

Die Installation von EclEmma geschieht am Leichtesten über die Update-Mechanismen von Eclipse. Anschließend steht ein neuer Launch-Modus für die Messung der Test-Abdeckung zur Verfügung. Über diesen Modus lassen sich beliebige Launch-Konfigurationen starten. Man kann somit nicht nur die Abdeckung von Unit-Tests messen, sondern die Abdeckung eines beliebigen ausführbaren Programms. Während ein Programm ausgeführt wird, zeichnet EclEmma im Hintergrund den durchlaufenen Code auf und zeigt die Ergebnisse am Ende übersichtlich an.

Der aktuelle Quellcode des Spring-Frameworks dient als Beispiel dazu, wie die Ergebnisse von EclEmma interpretiert werden können. Dazu wird zunächst mit dem Quellcode von Spring und seinen Tests ein Eclipse-Projekt angelegt. Eine eigene Launch-Konfiguration startet dann alle Unit-Tests von Spring. Nach dem Abschluss der Tests werden die Ergebnisse in einer eigenen View anzeigt:

Darstellung der Test-Abdeckung des Spring-Quellcodes

In dieser Darstellung sieht man zunächst den Anteil der von den Tests durchlaufenen Anweisungen im Verhältnis zu allen Anweisungen im Programmcode, und zwar gruppiert auf Ebene der Java-Pakete. Man kann beispielsweise erkennen, dass der vollständige Quellcode von Spring (im Verzeichnis src) zu gut zwei Dritteln durch die Tests abgedeckt wird, die Abdeckung der AOP-Pakete jedoch im Schnitt bei etwa 80% liegt.

Ausgehend von der Test-Abdeckung vollständiger Pakete macht es natürlich Sinn, tiefer in den Code einzusteigen, und die Test-Abdeckung einzelner Klassen oder gar Methoden zu untersuchen. Die nächste Abbildung zeigt beispielsweise die Test-Abdeckung einzelner Methoden der Klasse DefaultListableBeanFactory:

Test-Abdeckung einzelner Klassen und Methoden

Wenn man sich die absoluten Werte genauer anschaut, ist die in diesen Beispielen verwendete Maßeinheit Instruktionen vielleicht nicht ganz so verständlich. Daher bietet EclEmma die Möglichkeit an, auf andere Maßeinheiten umzuschalten, und zwar auf Zeilen, Code-Blöcke, Methoden und Klassen. Bei Zeilen versucht das Tool zu erkennen, ob mindestens eine Anweisung einer Codezeile durchlaufen worden ist. Als Code-Block gelten alle Anweisungen zwischen zwei Programmsprüngen, die zwangsläufig aufeinander folgend ausgeführt werden. Und die Einstellung Methoden bzw. Klassen bietet einen schnellen Überblick, ob bestimmte Methoden oder Klassen überhaupt durchlaufen wurden.

Durch diese tabellarische Übersicht ist es also relativ einfach, Code-Abschnitt zu finden, die zu einem relativ niedrigen Teil von Tests abgedeckt sind. Um zu verstehen, warum das so ist, muss man den fraglichen Code in einen Editor laden. Dort werden die durchlaufenen bzw. die nicht oder nur teilweise durchlaufenen Blöcke farblich hervorgehoben:

Ergebnisse der Test-Abdeckung, im Quellcode markiert

Alle Code-Abschnitte, die von irgendeinem Test durchlaufen wurden, sind grün hinterlegt, alle nicht durchlaufenen rot. Man kann somit erkennen, dass beispielsweise im oberen Abschnitt die Methode raiseNoSuchBeanDefinitionException durch keinen Test aufgerufen worden ist. Ebenso, und das wiegt eventuell schwerwiegender, wurde der ganze untere Abschnitt der Methode nicht durchlaufen.

Es findet sich aber auch eine in gelb markierte Zeile. Gelb bedeutet hier, dass diese Zeile teilweise durchlaufen worden ist, aber nicht vollständig. In diesem Fall heißt es, dass die Abfrage typeConverter != null nur eine der beiden Möglichkeiten ergeben hat, also true oder false, und somit nur eine der beiden möglichen Zuweisungen stattgefunden hat.

Mit Hilfe dieser farblich markierten Darstellung des Programmcodes lassen sich mit ein wenig Übung sehr schnell diejenigen Programmstellen aufspüren, die bisher bei den Tests zu kurz gekommen sind. In den meisten Fällen finden sich aber auch viele Code-Abschnitte, für die Tests vielleicht weniger wichtig sind. Ob eine Test-Abdeckung von 100% erstrebenswert ist, ist eine Frage, die diskutiert wird, die im normalen Projektalltag aber nur eine geringe Rolle spielt.

Fazit

Die Ermittlung der Test-Abdeckung kann einen wertvollen Beitrag für die Qualitätssicherung eines Projekts leisten. Sie nutzt zum einen den Entwicklern, die ihre Tests damit zielgerichtet schreiben können, und zum anderen der Projektleitung, die damit transparent prüfen kann, ob Vorgaben eingehalten werden.

Der Kennwert der Test-Abdeckung alleine ist jedoch nur bedingt aussagekräftig. Erst ein Review des durch die Tests durchlaufenen Programmcodes zeigt ein klareres Bild der Qualität der vorhandenen Tests. Ihren besonderen Nutzen zeigt die Messung der Test-Abdeckung vor allem dann, wenn sie in eine Build-Automatisierung eingegliedert ist und somit ein Vergleich der Werte im Verlauf der Zeit möglich ist.