Was sind Software Tests?
Glenford J. Myers und Corey Sandler definieren Software Tests in ihrem Buch “The Art of Software Testing”
folgendermaßen:
“Testing is the process of executing a program with the intent of finding errors”
Tests sind also der Prozess, ein Programm mehrfach auszuführen um Fehler zu finden.
Dies kann sowohl manuell durch Tester passieren als auch automatisch durch kleine Programme, die das
Textprogramm ausführen und testen. Im folgenden werden die automatischen Tests betrachtet.
Automatische Software Tests kann man im Grunde in zwei Kategorien aufteilen: Funktionale Tests und
Non-Funktionale Tests. Unter funktionalen Tests versteht man Tests, bei denen die Software oder Teile der
Software auf Korrektheit überprüft werden. Beispielsweise, ob sich die Software in einer bestimmten
Situation so verhält, wie in den Spezifikationen festgelegt wurde.
Nicht funktionale Tests beziehen sich auf Eigenschaften von Software, wie etwa die Performance oder das
Einhalten von Guidelines.
Welche Arten funktionaler Tests gibt es?
Es gibt viele verschiedene Arten funktionaler Tests, hier sollen aber nur drei Arten näher beleuchtet
werden. Diese sind Unit-Tests, Integration-Tests und System Tests.
Ein Unit-Test ist ein Test einer Komponente. Damit ein Test als Unit-Test gilt, muss der Input
kontrollierbar sein und die getestete Komponente keine Abhängigkeit zu einer anderen Komponenten haben. Ein
Beispiel wäre eine Methode, die den ersten Übergabeparameter durch den zweiten Übergabeparameter teilt. Hier
z.B. kann getestet werden, ob die Rechnung korrekt durchgeführt wird und ob das Teilen durch 0 behandelt
wird.
Ein Integration-Test findet statt, sobald mehr als eine Komponente gleichzeitig abgetestet wird.
Etwa wenn unsere Methode für das Teilen zweier Zahlen eine weitere Methode aufrufen würde.
System Tests oder auch End-to-End Tests sind Tests, die einen Prozess, die durch Programmspezifikationen
festgelegt sind, abtastet. UI-Tests sind ein Beispiel für solche Tests.
So könnte in einer Spezifikation für ein Android Programm stehen: Bei Druck auf den Floating Action Button
öffnet sich ein neues Fenster, in welchem ein neues Flugzeug hinzugefügt werden kann. Ein UI-Test würde dann
den Button drücken und überprüfen, ob sich tatsächlich dieses spezifizierte Fenster öffnet. Wie man
UI-Tests in Android mit Espresso schreibt, könnt ihr in dem zugehörigen
Blogbeitrag
nachlesen.
Was versteht man unter Code-Coverage?
Code-Coverage ist eine Metrik, die verwendet wird um zu messen, welche Teile der Software durch Tests
abgedeckt werden. Genauer heißt das, welche Teile des Codes durch das Ausführen der Tests durchlaufen
werden.
Hierbei ist noch zu beachten, dass es zwei Begriffe gibt, die zwar oft synonym verwendet werde, jedoch
unterschiedliches aussagen. Diese Begriffe sind Code-Coverage und Test-Coverage (Testabdeckung).
Während bei Code-Coverage ausschließlich die Abdeckung des Codes durch Ausführung der Tests berechnet wird,
versteht man unter Test-Coverage die vollständige Abdeckung des Programms durch alle automatischen und
manuellen Tests.
Code-Coverage wird meist von Tools wie etwa Cobertura
oder Jacoco berechnet und visualisiert.
Code-Coverage kann in verschiedenen Arten gemessen werden:
- Funktions-/Methoden Abdeckung: Hierbei wird das Verhältnis von definierten und aufgerufenen
Funktionen/Methoden gemessen.
- Anweisungsabdeckung: Bei dieser Art wird das Verhältnis von vorhandenen und aufgerufenen Anweisungen
gemessen.
- Abdeckung von Branches: Hier wird das Verhältnis von verschiedenen Möglichkeiten bei
Kontrollstrukturen, wie etwa bei einer if-else-Anweisung und den beim Test durchlaufenden Möglichkeiten
gemessen.
- Abdeckung von Codezeilen: Hierbei wird das Verhältnis von Codezeilen zu den im Test durchlaufenen Zeilen
gemessen.
Diese verschiedenen Messwerte können in einigen Fällen stark voneinander abweichen. Hier ein kleines Beispiel
anhand einer Methode:
public int divide(int numerator, int denominator) throws ArithmeticException{
if (denominator == 0) {
System.out.println("Cannot divide by zero");
throw new ArithmeticException();
} else {
return (numerator / denominator);
}
}
Für diese Methode würden wir folgenden Unit-Test schreiben:
@Test
public void test_divide() {
int result = divide(4, 2);
Assert.assertEquals(2, result);
}
Wenn wir anschließend die Coverage dieses Tests errechnen lassen, erhalten wir folgende Ergebnisse:
- Funktions-/Methoden-Abdeckung: 100% ⇨ Es existiert eine Funktion und diese wurde aufgerufen
-
Abdeckung von Branches: 50% ⇨ Der if-Fall wurde nicht abgedeckt
- Abdeckung von Codezeilen: 75% ⇨ Da nur der else-Fall durchlaufen wurde, sind nur 6 von 8 Zeilen
abgedeckt worden
Hier kann man bereits sehen, wie unterschiedlich die Messungen für verschiedene Typen der Code-Coverage
ausfallen können. Während die Funktionsabdeckung bei vollständigen 100% liegt, befindet sich die Abdeckung
der
Branches nur bei der Hälfte.
Code-Coverage kann also verwendet werden, um herauszufinden, welche Teile des Codes bereits mit Tests
abgedeckt worden sind. Sie kann jedoch nichts über die Qualität der Tests aussagen.
Der Test im obigen Beispiel führt die Funktion zwar aus und überprüft das Ergebnis, jedoch wird nur ein
möglicher Fall abgedeckt. Was wäre zum Beispiel, wenn 0 als Teiler übergeben wird?
Fügen wir noch folgenden Test für das Verhalten bei Übergabe von 0 hinzu, so erreichen wir auch 100% Line
und Branch Coverage:
@Test(expected = ArithmeticException.class)
public void test_divide_zeroAsDenominator_ShouldThrowException(){
divide(2,0);
}
Wurde diese Methode nun vollständig getestet? Die Antwort auf diese Frage lautet
nein, da das Verhalten der Funktion bei Übergabewerten, die nicht zu einem runden Ergebnis führen, nicht
getestet ist. Dennoch zeigte uns die Coverage hier eine hundertprozentige Abdeckung in allen Kategorien an.
Hier kann man bereits sehen, dass selbst bei vollständiger Abdeckung nicht garantiert ist, dass der
abgedeckte Code selbst vollständig getestet wurde.
Außerdem möchte ich an dieser Stelle noch erwähnen, dass es relativ einfach ist, eine hohe Code-Coverage zu
erreichen, indem Tests geschrieben werden, welche keine assert-Anweisungen enthalten. Diese haben nur einen
geringen Wert für das Projekt und sollten auch nicht geschrieben werden. Trotzdem würde ein solcher Test
Laufzeitfehler im durchlaufenen Code aufdecken, was jedoch für mich keine Entschuldigung ist assert freie
Tests zu schreiben.
100% Code-Coverage ein sinnvolles Ziel?
Das Thema 100% Code-Coverage wird in der Branche relativ kontrovers diskutiert. Grundsätzlich gibt es für
zwei verschiedene Ansichten zu diesem Thema:
1.) 100% Code-Coverage sollte immer angestrebt werden und ist ein wichtiges Qualitätsmerkmal des Codes. Es
gibt unterschiedliche Ansichten, wie die 100% erreicht werden können.
So kann 100% Abdeckung erreicht werden, indem man von vorne herein darauf achtet, testbaren Code zu
schreiben und
triviale Funktionen, wie etwa getter/setter entweder aus der Coverage auszuschließen oder gar nicht erst
zu schreiben. Hier vertreten einige auch die Meinung, dass nicht testbarer Code nicht existieren sollte.
Außerdem sollten nicht hauptsächlich Unit-Tests, sondern auch Integration Tests geschrieben werden.
2.) 100% Code-Coverage sollte nicht angestrebt werden, da sie zum einen nichts über die Qualität und
Vollständigkeit der Tests aussagt und das Erreichen höherer Coverage zum anderen ab einer gewissen
Schwelle mit zu hohem Aufwand verbunden ist. Außerdem ist bei einer festgelegten Coverage die Versuchung
groß, Tests statt zur Qualitätssicherung für die Abdeckung zu schreiben, was dem ursprünglichen Zweck
von Tests widerspricht.
Grundsätzlich herrscht jedoch Konsens darüber, dass ein Programm eine hohe Testabdeckung haben sollte.
Außerdem sagen auch Unterstützer der ersten Ansicht, dass Code-Coverage nichts über die Qualität der Tests
aussagt und 100% Abdeckung nicht 100% fehlerfreien Code bedeutet.
Ist es nun sinnvoll 100%-Code-Coverage in einem Projekt anzustreben?
Meiner Meinung nach ist es nicht sinnvoll, sich die 100% oder auch eine beliebige andere Zahl als Ziel für
die Code-Coverage zu setzen. Das soll jedoch nicht bedeuten, dass Code-Coverage eine nutzlose
Metrik ist. Ich finde jedes Projekt sollte eine hohe Code-Coverage anstreben, ohne einen festen Prozentsatz
festzulegen.
Code-Coverage ist eine sinnvolle Metrik, die man auf jeden Fall verwenden sollte. Durch sie kann man
erkennen, welche Module noch mehr Tests vertragen könnten. Außerdem bin ich der Meinung, dass eine hohe
Abdeckung dazu führt, dass weniger Bugs bis in die Produktivversion durchkommen. Dieser Eindruck wird wird durch eine
Analyse von Microsoft zu ihren Testbemühungen bestätigt.
Außerdem sollten nicht ausschließlich Unit-Tests verwendet werden, um eine hohe Code-Coverage zu erreichen.
Für mich spricht gegen ein Festlegen der zu erreichenden Code-Coverage die Tendenz, Tests nach maximaler
Codeabdeckung zu schreiben. Selbst wenn qualitativ hochwertige Tests geschrieben werden, werden
eventuell wichtigere Module nicht getestet. Insgesamt halte ich es für wichtiger, kritische Module mit Tests
abzudecken und für diese Module eine hohe Abdeckung zu erreichen, als Tests für die Gesamtabdeckung zu
schreiben.