Software-Testing: Eine Einführung

In der Entwicklungsphase einer Software kommen auf 1000 Zeilen Code 30 bis 85 Bugs. Beim User kommen noch ca. 0.5 bis 3 Fehler pro 1000 Zeilen Code an. Diese Differenz geht maßgeblich auf systematisches Testen der Software zurück.

Software Bugs waren am 4. Juni 1996 für das teuerste Feuerwerk aller Zeiten verantwortlich. Die Ariane 5 explodierte auf ihrem Weg ins All wegen eines Integer Overflows. Der Versuch, einen 64-Bit-Integer-Wert in einen 16-Bit-Speicherplatz zu speichern, resultierte in der Tragödie, die die ESA ca. 500 Millionen Dollar kostete. Dieser Zwischenfall zeigt eindrucksvoll wie wichtig sorgfältiges und vollständiges Testen ist. Wäre das System der Ariane 5 besser getestet worden, wäre wohl aufgefallen, dass wiederverwendete Programmteile der Ariane 4 für das neuere und schnellere Modell nicht mehr ausreichend waren.

Tests schreiben ist im Allgemeinen ein eher ungeliebter Job in der Softwareentwicklung. Kleinteilig muss das gesamte System nach potentiellen Fehlabläufen durchsucht werden. Das ist mühsam, zeitaufwändig und nicht zuletzt auch teuer. Unabhängig davon ist Testen enorm wichtig, da durch ausführliches Testen Fehlfunktionen vor der Auslieferung der Software erkannt und behoben werden können. Es muss auch nicht immer die Explosion einer Rakete sein, falsche Zugfahrpläne oder Fertigungslinien, die grundlos stehen bleiben, können Geld und Nerven kosten oder sogar gefährlich sein.

Im Folgenden werden wir uns 3 wichtigen Themen im Software Testing widmen: Strategien, Ebenen und Abdeckung. Zeitgleich betrachten wir den Ansatz des Test-Driven-Development, in dem der Grundsatz “Erst testen, dann implementieren” gilt. Hier wird die Idee verfolgt, dass Tests für Programmteile geschrieben werden, bevor sie implementiert werden. Diese Tests können dann im Nachhinein auch als Erweiterung der Spezifikation dienen.

Schwarze und weiße Kisten

Testing lässt sich grob mit 2 Strategien durchführen: strukturelles White Box-Testing und funktionales Black Box-Testing. Blackbox-Testing erhält seinen Namen dadurch, dass der implementierte Code selbst nicht betrachtet wird. Stattdessen werden Tests basierend auf der Spezifikation geschrieben. Die Funktionsweise des Programms wird abgefragt. Hierbei soll so viel spezifiziertes Verhalten wie möglich überprüft werden. Diese Form des Testing ist sehr aufwändig, da Tests mit der gegebenen Spezifikation abgeglichen werden müssen, die in der Regel nicht formalisiert vorliegt. Darum ist insbesondere diese Strategie auch schlecht automatisierbar. White Box-Testing betrachtet den implementierten Code direkt und soll so viel implementiertes Verhalten wie möglich abdecken. Hiermit können besonders gut strukturelle Zusammenhänge überprüft werden. Im Gegensatz zum Black Box-Testing ist das White Box-Testing sehr gut automatisierbar. Ein klassischer Nachteil des White Box-Testing ist allerdings, dass Entwickler ihre Whitebox-Tests in der Regel selbst schreiben und somit die Gefahr besteht, „um Fehler herum“ zu testen. Insbesondere in kleinen Firmen und Projekten ist es nämlich oft nicht möglich eigene Teams für das Testing abzustellen. Um dem entgegenzuwirken kann Test-Driven-Development eingesetzt werden. Da die Tests vor dem Code geschrieben werden, hat die beschriebene Betriebsblindheit weniger Einfluss auf die Funktionalität der Tests.

Testebenen

Software Tests werden häufig in 5 Ebenen unterteilt: Unit-Tests, Integrationstests, Regressions-Tests, System-Tests und Akzeptanztests. Die 5 Ebenen bauen hierarchisch aufeinander auf. Um ein Programm ausführlich zu testen werden alle 5 Ebenen benötigt. Unit-Tests In dieser niedrigsten Testebene konzentriert man sich auf die kleinsten testbaren Teile einer Software, z. B. die Methoden. Es geht darum, ob richtige Inputs richtig verarbeitet, falsche Inputs abgelehnt werden usw. Diese Tests werden meist von den Entwicklern selbst geschrieben oder direkt automatisiert erstellt. Wenn für den Test Inputs benötigt werden, die möglicherweise aus noch nicht implementierten Programmteilen stammen, werden gerne sog. Mocks verwendet. Mocks sind Attrappen für Elemente, die entweder nicht angefasst werden sollen oder eben noch nicht entwickelt sind. Letzteres geschieht insbesondere im Test-Driven-Development häufig, da alle Tests vor dem Code geschrieben werden. Die Tests laufen dann beispielsweise mit von Hand gebauten leeren Klassen, die noch fehlende Teile der Software ersetzen. Integrations-Tests Die in den Unit Tests einzeln getesteten Einheiten werden in Gruppen zusammengefasst und getestet. Diese Testebene ist insbesondere wichtig, um zu prüfen, ob die Einzelmethoden richtig ineinandergreifen. Regressions-Tests Technisch gesehen ist das keine Ebene, aber definitiv empfehlenswert in der Entwicklung ist das Regressionstesten. Nach jeder Änderung sollten alle verfügbaren Tests immer wieder neu durchgeführt werden, um sicherzustellen dass kein bisher funktionierender Teil beschädigt wurde. Da das sehr viel Arbeit bedeutet, geschieht das idealerweise automatisiert mit der Unterstützung geeigneter Tools. System-Tests Hier wird das System als Ganzes getestet. Die Interaktion der einzelnen Teile und die Kommunikation nach außen stehen im Vordergrund. Die Validierung wird anhand von Anwendungsfällen vorgenommen und ist üblicherweise sehr aufwändig. Darum werden Systemtests typischerweise in die Hände separater Testteams gegeben. An dieser Stelle sind GUI, Usability und Performance zum ersten Mal sinnvoll testbar. Akzeptanz-Tests Während der Akzeptanz-Tests wird mit echten Daten getestet. Diese Ebene unterteilt man in Alpha- und Beta-Tests. Alpha-Testing wird üblicherweise von den Entwicklern oder zugehörigen Teams durchgeführt. In den Beta-Tests dürfen das erste Mal die Endnutzer ran (insbesondere bekannt aus der „Beta-Phase“ der Spieleentwicklung). Mit Hilfe der ausgewählten Endnutzer möchte man so auch erfahren, ob alle erwarteten Fähigkeiten der Software gegeben sind. Dies ist wichtig, damit die benötigte Akzeptanz und der geplante wirtschaftliche Erfolg möglich wird.

Abdeckung

Alles Testen bringt nur wenig, solange es nicht möglichst vollständig geschieht. Zur Feststellung der Vollständigkeit gibt es im Wesentlichen 6 sogenannte Abdeckungen, die man betrachten sollte:

  • Zeilenabdeckung: Sehr intuitiv, hier wird darauf geachtet, dass jede einzelne Zeile Code getestet wird.
  • Anweisungsabdeckung: Jede Anweisung im Code soll mindestens einmal geprüft werden.
  • Zweigabdeckung: Jeder ausführbare Zweig des Programms soll aufgespürt und getestet werden.
  • Pfadüberdeckung: Alle möglichen Pfade des Programms sollen geprüft werden. Ein Pfad ist hier ein Codeausführungsszenario, d. h. ein Weg, den eine Testeingabe bei der Ausführung im Programmcode durchläuft. Man kann sich leicht ausmalen, wie viele Pfade es dort geben kann und dass es somit eine sehr große Anzahl von Testfällen braucht, um diese Pfade zu erreichen.
  • Bedingungsüberdeckung (Code): Jede Bedingung muss einmal mit TRUE und einmal mit FALSE getestet werden.
  • Bedingungsüberdeckung (Compiler): Je nach Compiler werden boolsche Ausdrücke zum Beispiel in if-Statements strikt oder nicht-strikt ausgewertet. Strikte Auswertung bedeutet, dass immer alle Ausdrücke einer booleschen Bedingung ausgewertet werden, um das Ergebnis zu berechnen, nicht-strikte Auswertung dagegen, dass die einzelnen Ausdrücke einer boolesche Bedingung nur solange ausgewertet werden, bis das Ergebnis feststeht. Im nicht-strikten Fall wird beispielsweise in einer Bedingung mit zwei durch UND verknüpften Ausdrücken der zweite Ausdruck nur dann ausgewertet, wenn der erste TRUE ergeben hat. Wenn er nämlich FALSE ist, kann man sich die Auswertung des zweiten Ausdrucks sparen, da das Ergebnis nicht verändert wird. Man kann dann also nicht prüfen, ob dieser zweite Ausdruck fehlerfrei funktioniert. Diese Optimierung zugunsten der Laufzeit erschwert an dieser Stelle das Testen.

Wie man sieht, ist Testen so vielfältig wie die Softwareentwicklung selbst und nicht weniger komplex. Durch Testen kann auch nie die Korrektheit des Codes verifiziert werden, sondern nur Fehlerbehebung durchgeführt werden. “Program testing can be a very effective way to show the presence of bugs but is hopelessly inadequate for showing their absence.” (Edsger W. Dijkstra, The Humble Programmer, ACM Turing Lecture 1972) Als Fazit kann man feststellen, dass kein Code wirklich vollständig getestet sein kann - es gibt auch keinen absolut fehlerfreien Code. Zudem kommt es in der Realität häufig zu Kosten-Nutzen-Abwägungen, die den Aufwand, den man ins Testen steckt, begrenzen. Testing bringt im ersten Schritt keine neue Funktionalität und somit auch keine zusätzlichen Ergebnisse, kann aber in mittlerer und langer Frist nicht nur Unfälle wie die Explosion der Ariane 5 verhindern, sondern auch jede Menge Geld und Zeit einsparen. Es lohnt sich also in jedem Fall, einen nennenswerten Aufwand in das Testen einer Software zu stecken.