So – nach langer, arbeitsbedingter Blog-Pause melde ich mich zurück! Jetzt ist aber endlich meine Promotion beendet und ich habe (hoffentlich) wieder Zeit für einigermaßen regelmäßige Beiträge.

Anfangen möchte ich direkt mit dem Thema, mit dem ich mich die letzten Jahre über beschäftigt habe: dem Testen von Software. Konkret möchte ich im Rahmen einer kleinen Artikelserie zum Thema meiner Dissertation hinleiten und es im letzten Teil auch kurz vorstellen. Also – los geht es!

Jeder, der mit Computern arbeitet, kennt es vermutlich: Man benutzt eine Software (für die eventuell sogar viel Geld bezahlt wurde) und – zack! – stürzt sie ab oder zeigt ein anderes unerwünschtes Verhalten – kurz: einen Fehler. Warum sich Fehler in Programme einschleichen, ist nicht verwunderlich: da sie auch nur von Menschen programmiert werden, ist es nur natürlich, dass hin und wieder auch Fehler gemacht werden. Interessanter – und auch oft gestellt, nicht selten mit empörtem Unterton – ist dagegen die Frage, warum es diese Fehler ins fertige Produkt schaffen und sich scheinbar vorher niemand die Mühe gemacht hat, sie zu suchen und zu beseitigen.

Der Vorwurf ist natürlich einfach erhoben; es darf jedoch nicht vergessen werden, dass es in vielen Programmen oft unzählige Anwendungsfälle gibt, die oft auch vom Nutzerverhalten über einen längeren Zeitraum abhängen. Diese alle im Voraus zu erahnen und zu überprüfen, ist – vorsichtig ausgedrückt – nicht unbedingt einfach. Was uns auch direkt zur eigentlichen Frage des Beitrags führt: Wie kann Software überhaupt getestet werden?

Die einfachste Möglichkeit ist natürlich, dass sich jemand hinsetzt und die Software ausprobiert (vorzugsweise übrigens nicht der Programmierer selber, da er nur testet, ob die implementierten Sachen funktionieren – und nicht, was alles nicht klappt!). Wie gerade erwähnt, ist das bei hinreichend komplexer Software (und das heißt, alles, was über das einfachste hinausgeht) eine überaus undankbare, wenig zielführend und kaum zu bewältigende Aufgabe. Erschwert wird dies noch dadurch, dass an einem Programm natürlich auch immer noch weiter programmiert wird. Jede Änderung am Programmcode erfordert im Grunde ein neues Testprogramm. Auf diese Art könnte kaum sichergestellt werden, dass eine Software keine Fehler enthält (nichtsdestotrotz hat das manuelle Testen natürlich seine Daseinsberechtigung; insbesondere Software wie Computerspiele mit vielen Nutzereingaben über einen langen Zeitraum werden ausgiebig manuell getestet).

Wenn manuelles Testen keine Lösung ist, muss ein automatisierter Prozess her. Und in der Tat gibt es verschiedene Möglichkeiten, Software automatisiert zu testen. Eine der grundlegendsten Methoden ist die der Komponententests.

Software ist in der Regel aus mehreren Einzelteilen oder Komponenten zusammengesetzt. Was genau eine Komponente darstellt, ist etwas unscharf definiert; es kann sich hierbei um eine einzelne Methode eines Programms, eine Klasse (Randnotiz: Was eine Klasse ist, muss noch in einer eigenen Artikelserie behandelt werden!), ein ganzes Modul oder auch um einen funktionell zusammengehörigen Verbund von kleineren Komponenten handeln. Für den weiteren Verlauf des Artikels soll gelten, dass mit einer Komponente eine einzelne Methode gemeint ist.

Komponententests testen also (zum Beispiel!) Methoden automatisiert. Doch wie genau läuft das ab? Stellen wir uns einmal vor, wir haben eine einfache Funktion normalize, die einen Vektor normiert, ihn also auf eine Länge von genau 1 bei Beibehaltung der Richtung transformiert; dies wird erreicht, indem jede Koordinate des Vektors durch seine Länge geteilt wird (sqrt ist die Funktion zur Berechnung der Quadratwurzel):

(1)  normalize: v ∈ R3R3:
(2)    l ∈ R, l ← sqrt( v02 + v12 + v22 )
(3)    return [v0/l, v1/l, v2/l]

Auf den ersten Blick ganz einfach. Diese Funktion kann getestet werden, indem für sie ein Testfall geschrieben wird. Einfach gesagt ist ein Testfall wiederum eine Funktion, welche die zu testende Komponente mit einer bestimmten Eingabe aufruft und prüft, ob das gelieferte Ergebnis den Erwartungen entspricht. Ein Testfall für die Funktion normalize könnte zum Beispiel so aussehen:

(1)  test ‘normalize’:
(2)    assert normalize( [1, 0, 0] ) = [1, 0, 0]
(3)    assert normalize( [0, 2, 0] ) = [0, 1, 0]
(4)    assert normalize( [3, 0, 4] ) = [0.6, 0, 0.8]

Das Schlüsselwort assert prüft, ob eine gegebene Aussage wahr ist oder nicht; falls nicht, produziert sie einen Fehler, der dem Programmierer einen Hinweis darauf gibt, dass sein Test nicht erfolgreich war. Obiger Testfall überprüft die Methode normalize demzufolge anhand von drei Werten. Schlägt keine der drei Überprüfungen fehl, muss normalize also korrekt, das heißt ohne Fehler, sein…

Wirklich?

Nein, natürlich nicht. Ein Testfall kann nämlich nur überprüfen, ob eine Methode Fehler enthält – er kann nicht prüfen, ob sie fehlerfrei ist! Unsere normalize-Methode ist ein wunderbares Beispiel hierfür, denn sie enthält einen Fehler (der dem aufmerksamen Leser natürlich schon längst aufgefallen ist): Wird als Eingabe der Nullvektor, also der Vektor [0, 0, 0] mit der Länge 0, verwendet, findet eine Division durch 0 statt und produziert damit einen Fehler. Der Nullvektor ist eigentlich nicht normierbar; da Computer aber dennoch mit diesem Fall umgehen können müssen, wird üblicherweise vereinbart, dass der normierte Nullvektor wieder der Nullvektor ist. Die Funktion normalize müsste demzufolge folgendermaßen aussehen:

(1)  normalize: v ∈ R3R3:
(2)    l ∈ R, l ← sqrt( v02 + v12 + v22 )
(3)    if l = 0:
(4)        return [0, 0, 0]
(5)    else:
(6)        return [v0/l, v1/l, v2/l]

Um dieses Versäumnis als Fehler zu entdecken, muss der Testfall auch entsprechend angepasst werden:

(1)  test ‘normalize’:
(2)    assert normalize( [1, 0, 0] ) = [1, 0, 0]
(3)    assert normalize( [0, 2, 0] ) = [0, 1, 0]
(4)    assert normalize( [3, 0, -4] ) = [0.6, 0, -0.8]
(5)    assert normalize( [0, 0, 0] ) = [0, 0, 0]

Wünschenswert wäre natürlich, wenn ein Test gleich alle möglichen Eingaben überprüfen würde. Dem steht allerdings entgegen, dass es davon in der Regel ziemlich viele gibt; allein die Anzahl der möglichen dreidimensionalen Vektoren, die ein Computer darstellen kann, ist viel zu groß, um sie in vertretbarer Zeit zu überprüfen. Man kann sich also nur einige wichtige Werte in Grenzbereichen (im Falle der dreidimensionalen Vektoren sind das zum Beispiel der Nullvektor, Vektoren mit nur einer nichtleeren Koordinate, mit zwei nichtleeren Koordinaten) und einige weitere Testwerte heraussuchen und die Korrektheit der zu testenden Komponente mit diesen Werten überprüfen. Schlagen derartige Tests nicht fehl, ist das zumindest ein Hinweis darauf, dass keine allzu groben Fehler in der Komponente vorhanden sind – Fehlerfreiheit beweist das jedoch wie gesagt nicht!

Die gesamte Funktionalität eines Programms lässt sich mit Komponententests natürlich nicht überprüfen, da jene erst durch das Zusammenspiel vieler Komponenten entsteht. Komponententests können demzufolge nur benutzt werden, um die Basisfunktionalitäten eines Programms (etwa in Form von Methoden) zu testen – aber das ist immerhin besser als nichts und führt in Kombination mit weiteren Testverfahren (Stichworte: Integrationstests, Systemtests) dazu, dass immerhin die gröbsten Fehler gefunden werden können.

Die Eigenschaft von Komponententests, dass sie lediglich Fehler finden, aber keine Fehlerfreiheit nachweisen können, gilt für sämtliche Testverfahren, egal ob manuell oder automatisiert. Das ist auch einer der Gründe, warum selbst in kommerziell vertriebener und viel getesteter Software Fehler enthalten sind: Es ist schlicht nicht möglich, sämtliche Fehler in einem Programm im Voraus zu entdecken (außer, die Korrektheit des Codes wird formal bewiesen – aber das ist ein ganz anderes Kapitel…). Wenn also das nächste mal ein Programm ein Fehlverhalten aufweist, welches doch eigentlich vorher schon hätte bemerkt und beseitigt werden müssen, immer daran denken: Programmierer sind auch nur Menschen. Und machen wie alle anderen auch Fehler.

Im nächsten Teil der Artikelserie soll es darum gehen, was zu tun ist, wenn ein Test fehlschlägt und wie ein Programmierer aus dem Fehlschlagen mehrerer Tests den möglichen Ort des Fehlers im Programmcode lokalisieren kann.

Kommentare (12)

  1. #1 michael
    Juli 14, 2017

    > Jetzt ist aber endlich meine Promotion beendet …

    Erfolgreich hoffe ich mal. Also: Glückwunsch.

  2. #2 rolak
    Juli 14, 2017

    Da ist er ja wieder, der neuerdings Frischpromovierte, nach gut drei Jahren Durststrecke…
    Beste Wünsche nachträglich, viel Lust am Bloggen möge ihn beschleichen.

  3. #3 Dr. Webbaer
    Juli 14, 2017

    Yup, Software zu erstellen ist Handwerk, Gratz btw.
    MFG + willkommen zurück + schönes WE,
    Wb

  4. #4 user unknown
    https://demystifikation.wordpress.com/2017/01/08/wordpress-ministatistik-diy/
    Juli 14, 2017

    Es gibt inzwischen auch Testframeworks, die in der Lage sind, bei wiederholten Tests mittels Pseudozufallsgenerator immer wieder andere Werte zu testen und bei scheiternden Tests zu versuchen, den Testfall zu vereinfachen. In Scala etwa ScalaCheck, dessen Vorbild aber m.W. aus der Haskellwelt stammt (dort: Quickcheck, wenn ich mich recht entsinne).

    Man schreibt dafür dann Regeln der Art, dass normalize (v (a, b, c)) = normalize (v (na, nb, nc)) sein muss für alle n, a, b, c. Das System generiert einem dann einen Haufen Test, vor allem Randtests, wie sie im Text erwähnt werden, bei denen ein oder mehrere Parameter 0 sind, außerdem beliebt Maximalwerte und Minimalwerte (negative Werte).

    Da drohen dann weitere Fehler aufzufliegen, nämlich Überlauf bei a², b² und c². (Ich nutze a, b, c statt v0, 1, 2 weil Indizees so umständlich einzugeben sind, wenn das Kommentarinterface das überhaupt zulässt – wäre wohl das Sub-Tag, oder mit Codetag und Annotation der Sprache und dann LaTeX-Code).

    In Scalacode sieht das ungefähr so aus:

    import org.scalacheck._

    object NormalizerProperties extends Properties ("Normalizer") {

    val fno = FrenkelNormalizer

    property ("normalized factor of vector equals normalized vector") =
    Prop.forAll ((a: Float, b: float c: Float, n: Float) => {
    fno.normalize (a, b, c) == fno.normalize (a*n, b*n, c*n)
    })

    property ("if two are 0, normalized result must be 1 at not-null-Position") = Prop.forAll ((a: Float) => {
    if (a != 0.0) {
    fno.normalize (a, 0, 0) == fno.Vector (1, 0, 0)
    fno.normalize (0, a, 0) == fno.Vector (0, 1, 0)
    fno.normalize (0, 0, a) == fno.Vector (0, 0, 1)
    }
    else {
    fno.normalize (a, 0, 0) == fno.Zerovector &&
    fno.normalize (0, a, 0) == fno.Zerovector &&
    fno.normalize (0, 0, a) == fno.Zerovector
    }
    })

    property ("a normalized vector (a, b, c) should be the inverse of a vector (-a, -b, -c)") =
    Prop.forAll ((a: Float, b: float c: Float, n: Float) => {
    fno.normalize (a, b, c) == (fno.normalize (-a, -b, -c)).inverse
    })
    }

    Führende Blanks werden trotz Code- oder Pre-tags leider unterdrückt.

  5. #5 Marcus Frenkel
    Juli 15, 2017

    @#3
    Random-Tests haben das Problem, dass sie nicht reproduzierbar sind, außer, man loggt alle Werte des letzten Test-Runs mit. Dann muss man aber immer noch hoffen, dass (bekannte) kritische Stellen auch mit abgedeckt werden. Aber als Ergänzung für reguläre (fixe) Tests ist das sicherlich kein schlechter Ansatz (wenn man denn die zusätzlichen Test-Zeiten tolerieren kann).

  6. #6 Laie
    Juli 17, 2017

    Die mangelnde Qualität von Softwareprodukten kann u.a. mit der Nachfrage nach schlechter Software erfolgreich aufrecht erhalten werden.

    Denn der Markt regelt das von selbst. Dumme/kluge Nutzer kaufen halt dumme/gute Programme, oder auch:

    Die billigeren Tests führt der Endkunde selbst durch, der für jede neue “Version” zahlen darf, wenn er sich einen entsprechenden “Servicevertrag” aufbinden hat lassen.

    Der im Beitrag geschriebene positive akademische Ansatz zu bevorzugen. Wie kann man das dem Markt erklären, dass er sich danach zu richten hätte?

    • #7 Marcus Frenkel
      Juli 19, 2017

      Dem stimme ich nicht zu. Der Vorwurf, die Firmen würden lieber die Endkunden testen lassen, um Geld zu sparen, ist schnell und oft erhoben. Die Fehlerproblematik besteht aber genauso bei Open-Source-Produkten und trotz umfangreicher Testsuites.

  7. #8 Laie
    Juli 19, 2017

    @Marcus Frenkel
    Korrekter Weise variiert die Qualität der Software von Firma zu Firma, einige Testen mehr, andere weniger. Testen selbst sagt noch immer nichts über andere interne Eigenschaften und Qualitätsmerkmale, abgesehen von der Korrektheit und Geschwindigkeit eines Algorithmus aus.

    Richtig, das trifft auch bei Open-Source-Produkten zu. Weder ist das eine weiss, noch das andere schwarz und umgekehrt.

  8. #9 user unknown
    https://demystifikation.wordpress.com/2017/07/12/cat-ten-ohm/
    Juli 19, 2017

    Random-Tests haben das Problem, dass sie nicht reproduzierbar sind, außer, man loggt alle Werte des letzten Test-Runs mit.

    Man muss nur den Wert, mit dem der Zufallszahlengenerator initialisiert wurde, loggen.

    Dann muss man aber immer noch hoffen, dass (bekannte) kritische Stellen auch mit abgedeckt werden.

    Nein. Wenn man kritische Stellen kennt/findet, dann fasst man diese in eigene Testfälle. M.W. arbeiten die oben genannten Frameworks per se so, allgemein kritische Fälle (INT_MAXVALUE, INT_MINVALUE, 0 usw.) immer zu prüfen.

  9. #10 user unknown
    https://demystifikation.wordpress.com/2017/07/06/cafe-zeitsprung/
    Juli 20, 2017

    Nachtrag:
    Soeben über einen Vortrag von Hughes gestolpert, in dem zum Startzeitpunkt für 2 Folien von QuickCheck die Rede ist.

    Was ich wohl zu erwähnen vergaß: QuickCheck und seine Nachbildungen vereinfachen auch, wenn sie auf einen Fehler stoßen, den Fehler so weit möglich. Sehr hilfreiche Sache, das.

  10. #11 robert
    Juli 23, 2017

    user unknown,
    random Generatoren sind mir ein Rätsel.
    Einerseits braucht man einen Algoritmus um Zufallszahlen zu erzeugen, andererseits sollen die Zahlen zufaällig sein. Wie passt das zusammen?

  11. #12 user unknown
    https://demystifikation.wordpress.com/2015/12/17/slavoj-zizek/
    Juli 23, 2017

    Das passt wg. nachlässiger Terminologie zusammen. Oft sind die Zufallszahlengeneratoren in Wahrheit Pseudozufallszahlengeneratoren. Dazu gibt es schon X Artikel im Netz.
    Mit unvorhersehbarem Input über Tastatur, Mikrofon, Webcam, WLAN-Chip, Temperatursensoren usw. kann man aber echten Zufall hineinmischen und echte Zufälligkeit erzeugen. Für sehr viele Fälle genügt aber ein Pseudozufallszahlengenerator.