Unittests und Coverage – Wie viel Testabdeckung brauche ich?

Unit Tests sind schon etwas Tolles! Unit Tests werden in der Regel vom Entwickler selber parallel zur Entwicklung geschrieben. Es wird nicht zwingend zusätzliches Testpersonal benötigt (auch wenn es schwer zu empfehlen ist weitere Personen beim Softwaretest hinzuzuziehen). Außerdem werden hierbei schon sehr früh in der Entwicklung Fehler aufgedeckt und unter Umständen werden diese Fehler sogar bis auf die verursachenden Zeilen im Code eingegrenzt. Fehler, die hierbei gefunden werden, müssen daher nicht später in aufwendigen Debug Sessions gesucht werden. Das gilt natürlich auch für statische Code-Analyse und Code-Reviews. Zumindest gegenüber dem Code Review allerdings, haben Unittests den Vorteil, dass diese automatisierbar sind, und somit Fehler aufgrund von Codeänderungen ganz von alleine finden. Wenn Unit Tests also wirklich so klasse sind, sollte man denken:

“je mehr Code Zeilen ich als Entwickler damit abdecke, desto mehr Fehler finde ich logischerweise”

Sollte man denken…

Was ist eigentlich coverage?

Erst einmal etwas Theorie: Coverage bezeichnet den Grad, zu dem eine Software durch Tests abgedeckt wird. In der Regel wird dies ausgedrückt in Prozent. Dabei gibt es verschiedene Möglichkeiten coverage zu berechnen. Zwei der gängigsten sind dabei:

  • Statement-coverage: Der prozentuale Anteil an abgedeckten statements im Code.
  • Branch-coverage: Der prozentuale Anteil der beim Test durchlaufenen Verzweigungen.

Als Beispiel: Gibt es in einem switch-case einen default-case, in dem wirklich nichts passiert (natürlich sollte hier mindestens ein Kommentar stehen, der erklärt warum hier nichts passieren muss), so muss dieser default-case beim Test nicht durchlaufen werden, um die statement-coverage auf 100 % zu treiben. Es ist ja schließlich kein statement enthalten. Für branch-coverage muss jedoch bei jeder Entscheidung jeder mögliche Ausgang getestet werden. Das heißt in unserem switch-case Beispiel muss jeder case durchlaufen werden um zu 100 % statement-coverage zu gelangen. Ein weiteres Beispiel zeigt die Abbildung unten. Das Bild zeigt eine Coverage Berechnung der Funktion “testFunction”. Grün hinterlegte Zeilen wurden bei mindestens einem Test durchlaufen. Rot hinterlegte hingegen wurden bei keinem Test durchlaufen. Bezogen auf die erste if-else-Anweisung (Zeile 14-19) ergibt sich ein Statement-coverage von 100 %, da alle statements durchlaufen wurden. Die Branch-coverage für diesen Bereich liegt hingegen nur bei 50 %. Bei der zweiten if-else-Anweisung (Zeile 21-26) ergibt sich jeweils eine Statement- und Branch-coverage von 50 %. Darüber hinaus gibt es noch weitere Möglichkeiten – wie “function-coverage” oder “modified condition/decision coverage” – um die Testabdeckung von Unit Tests zu berechnen. Auf diese möchte ich der Einfachheit halber an dieser Stelle allerdings nicht eingehen.

Das Problem mit der coverage

Vor kurzem habe ich in einem Projekt gearbeitet, in dem die Prozesse des Kunden 100 % Statement coverage forderten. Die meisten anderen Entwickler, denen ich davon erzählte reagierten geschockt: „Was, sowas gibt es?”. “…aber ergibt sowas Sinn?”

Ja sowas gibt es natürlich! Aber Sinn macht es aus folgenden Gründen nicht unbedingt:

Wer immer in der Planung von Projekten beschäftigt ist, scheint grundsätzlich zu wenig Zeit für Software-Tests einzuplanen. Warum das so ist, füllt sicher einen weiteren Blog Beitrag. Aber die Konsequenzen liegen auf der Hand. Schreiben die Prozesse 100% coverage (egal welche man hier zur Berechnung heranzieht), bedeutet dies erst einmal einen gigantischen Berg Arbeit, den der Entwickler kaum abarbeiten kann. Zumindest nicht, wenn er seine Arbeit gewissenhaft verrichten will (nehmen wir an dieser Stelle an, der Entwickler schreibt die Unittests für seinen Code selbst). Zum Glück liefern ihm die Prozesse aber auch direkt einen Ausweg! Gefordert sind lediglich x % statement- und eventuell y % branch-coverage. Wie diese erreicht werden sollen, schreibt in der Regel keiner vor. In einer idealen Welt würde sich ein Entwickler natürlich trotzdem gewissenhaft jedem Unit Test widmen. Auch wenn die nur öde völlig linear geschrieben getter-Funktionen testen, bei denen man jeden Fehler schon von weitem sieht. In der Praxis bleibt ihm dafür aber entweder gar keine Zeit, oder das Schreiben von lauter völlig unnützen Unit Tests lässt den Entwickler soweit abstumpfen, dass er das Schreiben solcher Tests als äußerst nervige Pflicht empfindet.  Eine hohe coverage führt so ggf. eher dazu, dass die Qualität des einzelnen Testcases sinkt. Schließlich fragt am Ende keiner mehr, ob der Test mehr als

“3 = 3 ?”

abprüft. Zumindest nicht, wenn das automatisch generierte Protokoll eine schön hohe coverage ausspuckt und diese Zahl dann sogar noch mit grüner Farbe hinterlegt.

Ihr Ansprechpartner:
M.Sc. Björn Schmitz, Software Entwickler
E-Mail: schmitz@medtech-ingenieur.de
Tel.:  +49 9131 691 240
Benötigen Sie Unterstützung bei der Entwicklung Ihres Medizingeräts? Wir helfen gerne! MEDtech-Ingenieur bietet Hardware-, Software Entwicklung, Systems Engineering und Beratung aus einer Hand. Nehmen Sie Kontakt auf.

Kontakt aufnehmen

Test Abdeckung ist demnach also nicht unbedingt ein Maß dafür wie wenig Fehler eine Software enthält. Zu einem ähnlichen Ergebnis kam auch eine Arbeit der Universität Göteborg zusammen mit Ericsson. Diese wurde unter dem Titel “Mythical Unit Test Coverage” veröffentlicht und beschreibt eine Studie, bei der versucht wurde eine Korrelation zwischen Unit Test coverage und fehlerfreiem Code zu finden. Eine klare Korrelation zwischen Testabdeckung und gefundenen Fehlern stellten die Autoren nicht fest. Es wurde aber sehr wohl eine Korrelation zwischen Komplexität, Größe der Softwaredateien (line of Code), sowie Anzahl der Code-Änderungen und den gefundenen Fehlern festgestellt. Es könnte also viel wirkungsvoller sein, in das Einhalten der Kodierrichtlinien, gut gekapselten Code, und eine klare Softwarearchitektur (die natürlich auch eingehalten werden sollte) zu investieren, als exzessives Unit Testing zu Betreiben.

Warum halten sich coverage Softwaremetriken trotzdem?

Diese Frage ist im Prinzip einfach zu beantworten. Die Sinnhaftigkeit von Tests lässt sich nur messen, wenn man sich intensiv in den Source Code eingräbt und diesen versteht. Eine Zahl im Protokoll gegen den Sollwert abzugleichen ist selbstverständlich deutlich einfacher. Wenn später dann ein Fehler im Feld Auftritt kann man auf dieses Protokoll verweisen und sagen „wir haben nach bestem Wissen und Gewissen unsere Software getestet. Mit Fehlern konnten wir daher unmöglich rechnen”. Das sichert einen natürlich ab und im besten Fall erreicht man mit der erzwungenen Testabdeckung einen gewissen “Minimalstandard”. Einen fehlerfreien Code erreicht man hiermit aber sicher nicht.

Was kann man besser machen?

Hier wird es knifflig. Wichtige Bausteine für einen (möglichst) fehlerfreien Code sind die bereits angesprochenen Maßnahmen:

  • Geringe Code-Komplexität
  • Gute Kapselung von Software und keine überdimensional großen Softwaredateien
  • Eine klare und gut durchdachte Softwarearchitektur, die auch vom Entwickler eingehalten wird (und deren Einhaltung selbstverständlich über Code-Reviews überprüft wird)

Darüber hinaus sollte auf Unit Tests – aus den eingangs Beschrieben und natürlich völlig unstrittigen Vorteilen – nicht verzichtet werden. Mein Vorschlag wäre allerdings für ein Projekt (oder besser für alle Softwareprojekte in einem Unternehmen) ein Konzept zu erarbeiten, was getestet werden sollte. Das kann zum Beispiel über eine Checkliste erfolgen:

Implementiert die Funktion Risikomaßnahme?  –> testen

Könnte die Funktion bei einem Fehlverhalten zu einem Schaden des Anwenders führen –> testen

Bei einem Code Review (das in der Regel eh durchgeführt werden muss) überprüft dann ein Entwickler – der den Code NICHT geschrieben hat – ob jede Funktion, die ins Raster fällt auch einen entsprechenden Test hat. Dies ermöglicht den Entwicklern sich auf das Wesentliche zu konzentrieren. Wer es genauer wissen will, führt stichprobenartig ein Review der Unit Tests durch. Werden hier viele Probleme gefunden, erweitert man das Suchraster. Führt man diesen Prozess kontinuierlich begleitend zur Entwicklung durch, wird der Entwickler ganz von alleine anfangen, bessere Tests zu schreiben.

Natürlich könnte man sich jetzt vorstellen, dass die Reviewer beim Aufdecken von Fehlern vorsichtig sind, um es sich nicht mit dem Entwickler zu verscherzen. Oder die Entwickler sprechen sich sogar ab, was bei den Stichproben gereviewed wird. Das halte ich persönlich für sehr unwahrscheinlich. Schließlich wollen die wenigsten Entwickler fehlerhaften Code produzieren. Es kann aber natürlich nicht völlig ausgeschlossen werden. Um solche Bedenken zu zerstreuen empfehle ich zum Review oder zum Schreiben der Tests einfach externe Kräfte zu verwenden, z.B. ein Entwickler aus einer anderen Abteilung. Falls kein entsprechender Entwickler im Unternehmen zur Hand ist, kann es auch sinnvoll sein, einen externen Dienstleister zu beauftragen. Solch ein externer Tester hat unter anderem folgende Vorteile:

  • Der Tester kennt den Entwickler nicht persönlich und kann Code und Unit Tests daher mit einer gewissen Neutralität begegnen.
  • Der Tester sieht Softwarearchitektur und Kodierrichtlinien zum ersten Mal und hat sich noch nicht daran gewöhnt, wenn sich gewisse Abweichungen in der Entwicklergruppe eingebürgert haben.
  • Ein projektinterner Entwickler, der den Code eines anderen reviewed, könnte sich davor fürchten zu viele Fehler aufzudecken die ihn “pedantisch” erscheinen lassen. Denn dies könnte wiederum dazu führen, dass der Autor sich das nächste Mal rächt und den Code des Testers ebenso kritisch auseinandernimmt. Ein externer Tester hat dieses Problem natürlich nicht.
  • Ein externer Entwickler als Tester, der sonst nicht mit den Entwicklern zusammenarbeitet, vergleicht automatisch Entwicklungsprozesse und die entwickelte Software mit den Prozessen und dem Code aus anderen Projekten. So findet er eventuell Schwachstellen, welche die projektinternen Entwickler übersehen.

Fazit

Unit Tests sind eine wichtige Säule im Software-Test, die früh und zielgerichtet Fehler aufdecken kann. Die Testabdeckung alleine sagt allerdings nichts über die Qualität des Codes aus. Ein Testkonzept, das ausschließlich auf hohe Testabdeckung setzt, kann unter Umständen sogar die Qualität der einzelnen Tests verschlechtern. Wichtiger ist es ein gutes Testkonzept zu entwickeln und festzulegen, wer was zu testen hat und wer dies überprüft. Ein (Projekt-) externer Softwaretester ist dabei immer hilfreich. Es bleibt jedoch dabei: Das beste Rezept für fehlerfreien Code ist eine gute Softwarearchitektur, die in gut lesbare und nicht übertrieben komplexe Softwaremodule übersetzt wird.

Auch interessant:

Code-Reviews in der Medizintechnik

Code-Review oder kein Code-Review? Das ist hier die Frage. Um Software-Qualität sicherzustellen, gibt es verschiedene Ansatzpunkte. Statische Codeanalyse, automatisierte Unittests, Integrationstests, Smoke…

Björn Schmitz

Seit Juli 2017 gehöre ich zum MEDtech-Ingenieur Team und bin hier vor allem als Firmwareentwickler tätig. Schon in kürzester Zeit konnte ich an vielen spannenden Projekten aus dem Bereich Medizintechnik, aber auch aus anderen Bereichen mitwirken.

Getagged mit: , , , ,
Ein Kommentar zu “Unittests und Coverage – Wie viel Testabdeckung brauche ich?
  1. Heiko Schmidt sagt:

    Hallo

    sehr interessanter Beitrag und viel Wahrheit. Viele Entwickler sehen es auch als Sport, auf 100% Testabedeckung zu kommen (egal welche Art von Testabdeckung).
    Viel wichtiger ist es jedoch sinnvolle Tests zu schreiben.
    Ein anderer Begriff für Testkonzept ist auch Teststrategie.
    Um jedoch auf eine gute Teststrategie zu kommen und diese auch glaubhaft verteidigen zu können, benötigt es als Grundlage eine gute Software-Architektur.

    Basierend auf der Software Architektur habe ich dann die Möglichkeit zu entscheiden, für welche Software Units ich Unit Tests mache und wo ich erst im Zuge der Integration verifiziere.

    Zudem darf man nicht vergessen, daß Qualität vorne im Projekt anfängt (konstruktive Maßnahmen). Im Testing sehe ich nur noch den Ist-Zustand und kann zeigen wie die Qualität tatsächlich ist.

    Eine weitere sehr wichtige Aufgabe von Unit Tests ist es, im Zuge von Änderungen zu zeigen, dass die durchgeführte Änderung keine Seiteneffekte hat. Hier hilft eine hohe Abdeckung auf alle Fälle ein sicheres Gefühl zu entwickeln, dass alles wie bisher funktioniert.
    Hier sehe ich den größten Nutzen von Unit-Tests mit hoher Abdeckung.

    Metriken halte ich generell für sinnvoll, aber kein blindes Vertrauen. Jede ermittelte Metrik muss man meistens hinterfragen.
    Gute Ergebnisse erzieht man, wenn verschiedene Metriken kombiniert werden.
    Habe ich eine Software Unit mit hoher Komplexität (z.B. McCabe > 20) und einer geringe Coverage, so könnte das ein klares Signal sein, die Testabdeckung zu erhöhen, oder aber ein Refactoring zu machen und somit die Komplexität zu reduzieren.
    Jedoch muss der Code betrachtet werden und dann eine Entscheidung getroffen werden (und dokumentiert). Nur anhand der Metriken zu entscheiden, halte ich für schwierig.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.