29.07.2020

Sinnvolles Testen - Hotspots statt Coverage

Welcher Softwareentwickler kennt nicht die Forderung nach 80% Test-Coverage? Die Zahl 80 steht an dieser Stelle stellvertretend für eine willkürliche, vom Management vorgegebene Zahl. Die 80%-Forderung impliziert zwei interessante Fragen:

  1. Warum überhaupt Test-Coverage und
  2. warum genau 80%?

Die erste Frage ist relativ einfach zu beantworten. Einer der wichtigsten Gründe für das Schreiben automatisierter Tests ist Regressionssicherheit. Der Begriff Regression steht in der Softwareentwicklung für einen Fehler, der in der Vorgängerversion nicht vorhanden war. Entsprechend bedeutet Regressionssicherheit das Absichern der Software gegen Regressionen. Und genau das tun automatisierte Tests, indem sie Regressionen automatisch aufzeigen. Je mehr Code von automatisierten Tests abgedeckt ist, desto größer ist die Chance, dass Regressionen nach Änderungen automatisch entdeckt werden.

Wenn Coverage gut ist, warum wird dann von vielen Teams gefordert, dass Tests nur 80% und nicht 100% ihres Codes abdecken sollen? Was ist, wenn der einen Seiteneffekt enthaltene Code in den 20% nicht abgedeckten Code steckt? Oder was ist, wenn Tests zwar alle Zweige des Codes durchlaufen, dabei aber Grenzwerte außer Acht lassen? In diesem Fall würden auch 100% Test-Coverage den potenziellen Seiteneffekt nicht aufzeigen, der z.B. entsteht, wenn die Funktion mit ungültigen Parametern aufgerufen wird.

Meistens stiftet die 80%-Forderung nur Verwirrung und sorgt für Unverständnis bei Entwicklern. Nicht selten sorgt die Forderung dann für blindes Testschreiben, das ausschließlich der Einhaltung von Regeln dient, weil die Entwickler wissen, dass das Management zum Sprint-Ende den SonarQube-Report kontrolliert. Dann doch lieber noch schnell ein paar Tests für die Getter und Setter nachliefern. Das geht schnell und treibt die Coverage in die Höhe, ohne das man sich besonders anstrengen muss.

Wenn Coverage gut, die Forderung nach einer bestimmten Prozentzahl hingegen fragwürdig ist, was bleibt uns dann? Eine vielversprechende Antwort auf diese Frage liefert Adam Tornhill in seinem lesenswerten Buch Software Design X-Rays [1]. Tornhill führt den Begriff des Hotspot für Code ein, der zum einen komplex ist und zum anderen häufig geändert wird. Komplexer Code ist anfällig für Regressionen. Häufige Änderungen erhöhen die Regressionswahrscheinlichkeit zusätzlich. Dazu ein kurzes Beispiel. Der folgende Konsolen-Dump wurde mit Hilfe des LegacyLab-Tools hs [4] erstellt und zeigt eine Hotspot-Analyse des Spring JPA Git-Repositories vom 23. Juli 2020:

$ hs -url https://github.com/spring-projects/spring-data-jpa.git

Commits File                                                                               Lines   Complexity
103     org/springframework/data/jpa/repository/support/SimpleJpaRepository.java           445     6.50
101     org/springframework/data/jpa/repository/sample/UserRepository.java                 256     4.12
87      org/springframework/data/jpa/repository/query/QueryUtils.java                      403     7.22
58      org/springframework/data/jpa/repository/query/JpaQueryMethod.java                  204     6.27
57      org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java          166     6.14
57      org/springframework/data/jpa/repository/query/AbstractJpaQuery.java                242     9.02
55      org/springframework/data/jpa/repository/query/JpaQueryCreator.java                 218     11.30
54      org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java                210     9.14
48      org/springframework/data/jpa/repository/query/JpaQueryExecution.java               183     7.93
47      org/springframework/data/jpa/repository/support/JpaMetamodelEntityInformation.java 248     8.71
44      org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java          111     9.37
43      org/springframework/data/jpa/repository/query/StringQuery.java                     428     11.02
43      org/springframework/data/jpa/repository/query/ParameterBinder.java                 42      6.10
38      org/springframework/data/jpa/repository/query/SimpleJpaQuery.java                  37      7.14
37      org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java     69      6.38
36      org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java      65      4.31
36      org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java   153     6.01
35      org/springframework/data/jpa/domain/sample/User.java                               198     7.66

Der Auszug zeigt eine Liste von Java-Quelldateien, geordnet nach Anzahl Commits, sowie deren Größe und Komplexität. Die Datei SimpleJpaRepository.java (Zeile 1) belegt mit 103 Commits und einer Komplexität von 6.5 Platz eins der Liste. Die zugrundeliegende Komplexitätsmetrik basiert auf der Einrückungstiefe des Sourcecodes und korreliert im Ergebnis mit deutlich aufwändigeren Metriken, wie z.B. der Cyclomatic Complextity-Metrik [3].

Die Nutzungsidee der Hotspot-Metrik ist folgende: Komplexität erhöht die Wahrscheinlichkeit des Auftretens unerwünschter Seiteneffekte. Wird die Datei zudem häufig und von unterschiedlichen Personen geändert, erhöht sich die Regressionswahrscheinlichkeit zusätzlich. Folglich sollte die Testabdeckung einer Datei umso besser sein, je komplexer diese ist und desto häufiger sich die Datei ändert. Dateien, die selten geändert oder deren letzter Commit schon eine Weile her ist, sind selbst bei hoher Komplexität weniger anfällig für Regressionen. Wo nichts geändert wird, geht auch nichts kaputt. Die Datei StringQuery.java (Zeile 12) ist ein Beispiel für solch einen Kandidaten. Die 43 Commits mit einer Komplexität von 11.02 suggerieren zunächst erstmal nur eine hohe Komplexität bei mittlerer Änderungshäufigkeit. Für eine Detailanalyse brauchen wir mehr Informationen, die uns ein git-log-Aufruf liefert:

$ git log src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java

commit e17aef1d2afa52ef4e0813ec32c977a520eac31e
Author: Hyunjin Choi <hyeonisism@gmail.com>
Date:   Fri Feb 7 00:39:04 2020 +0900

    DATAJPA-1676 - Fix some typos in JavaDoc.

    Original pull request: #412.

commit e27933455efa6d1821dea23abd2bbe109b5d59a7
Author: Mark Paluch <mpaluch@pivotal.io>
Date:   Tue Jan 7 08:52:02 2020 +0100

    DATAJPA-1656 - Update copyright years to 2020.

commit cde0b1dacf85cad38aa28c3027b77ea7ec790841
Author: Spring Operator <sagan-ops@gopivotal.com>
Date:   Fri Mar 22 00:38:37 2019 -0700

    DATAJPA-1512 - URL Cleanup.

    This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener).

Ein erster Blick auf den Log-Auszug zeigt, dass die Datei zuletzt im Februar 2020 geändert wurde. Auf den zweiten Blick sieht man, dass die letzte wirkliche Code-Änderung im März 2019 stattgefunden hat. StringQuery.java ist zwar komplex, aber ziemlich stabil und deshalb kein Kandidat für das Erhöhen der Test-Coverage.

Diese Beitrag hinterfragt die Sinnhaftigkeit von willkürlich gesetzten Test-Coverage-Vorgaben und plädiert stattdessen für sinnvolles Testen. Im Vordergrund steht dabei die Überlegung, das wir mitbekommen wollen, wenn eine Codeänderung etwas zerbricht. Die beschriebene Hotspot-Analyse eignet sich besonders für nachträgliche Erhöhen von Test-Coverage in Legacy-Projekten. Statt blind die Coverage hochzutreiben, wird vorher nach lohnenswerten Testkandidaten geschaut. Dieses Vorgehen nimmt den Druck aus nachgelagerten Coverage-Projekten, indem es die Arbeit basierend auf Hotspots priorisiert und genau aufzeigt: an folgenden Stellen ist dein Code zerbrechlich, hier solltest du etwas tun.

Referenzen

Zurück