Codeüberdeckung garantiert keine Qualitätssoftware

von Prof. Dr. Andreas Spillner und Prof. Dr. Karin Vosseberg

Gezeigt am Beispiel TDD

Agile Praktiken wie Testgetriebene Softwareentwicklung und damit einhergehend die Diskussion über Testautomatisierung haben erfreulicherweise die Aufgabe des Testens stärker in den Fokus der Softwareentwicklung integriert. Doch das morgendliche Schielen auf die nächtlichen Testläufe und die erreichte Codeüberdeckung gibt allein noch keine ausreichende Basis für die Qualitätseinschätzung der entwickelten Software. An zwei einfachen Beispielen wird gezeigt, dass ein fehlerfreier Lauf der Testfälle und eine 100%ige Codeüberdeckung keine Garantie für Fehlerfreiheit sind. Werden die Testfälle systematisch, also unter Verwendung von Testverfahren, erstellt, kann eine Qualitätsaussage getroffen werden.

Qualitätssoftware

Testgetriebene Softwareentwicklung (TDD – Test-Driven Development) als eine der agilen Praktiken hat den Stellenwert des Testens in der Softwareentwicklung deutlich erhöht. Ausgehend von der Idee als Designmethode [Marin 06] wird testgetriebene Softwareentwicklung gerade im Kontext agiler Softwareprojekte immer häufiger auch als qualitätssichernde Maßnahme propagiert. Experten für agile Softwareentwicklung werben mit Aussagen wie „Testgetriebene Entwicklung (…) bedeutet, dass Tests vor dem Produktivcode geschrieben werden, um so die Softwareentwicklung zu steuern. So entsteht Qualitätssoftware mit sehr hoher Testabdeckung, und es wird nur das entwickelt, was auch tatsächlich benötigt wird“ [it-agile AT]. In dieser Aussage wird ein wesentlicher Vorteil testgetriebener Softwareentwicklung deutlich: Fokussieren auf das, was tatsächlich benötigt wird, und kein Überladen der Systeme mit ungenutzter Funktionalität. Doch ist „wartbare Qualitätssoftware“ dadurch garantiert, dass „kein ungetesteter Code“ vorhanden ist [it-agile TDD]?

Testgetriebene Softwareentwicklung ist modern, und wir finden viele Beschreibungen in Büchern, Artikeln oder Blogs über die prinzipiell einfache Vorgehensweise (hier eine kleine Auswahl [Langr 13], [Ambler], [wiki TDD], [it-agile TDD]): Wir zerlegen unsere zu implementierende Funktionalität in kleine (Mikro-)Iterationen, die in nur wenigen Minuten zu lösen sind. Schreiben einen oder mehrere Testfälle für die Iteration. Danach implementieren wir genau so viel Code, um diese Testfälle zu erfüllen. Wenn dies der Fall ist, können wir nach einem eventuellem Refactoring mit der nächsten Iteration fortfahren.

Test-driven development ensures in this way that all written code is covered by at least one test. This gives the programming team, and subsequent users, a greater level of confidence in the code“ [wiki TDD]. Wenn der gesamte Programmcode durch mindestens einen Testfall überprüft wird, dann haben wir TDD erfolgreich durchgeführt.

Die Auswertung der Anweisungsüberdeckung zeigt uns, ob wir passend entwickelt haben. Haben wir keine 100 % Anweisungsüberdeckung, haben wir entweder mehr an Funktionalität implementiert als in dieser Iteration gefordert oder die Testfälle spezifizieren nicht korrekt die gewünschte Funktionalität. Haben wir aber auch ausreichend getestet?

Anhand von zwei sehr einfachen Beispielen wollen wir verdeutlichen, warum die Codeüberdeckung allein nicht ausreichend für eine Qualitätsbetrachtung ist, sondern ein sehr trügerisches und somit riskantes Qualitätskriterium.

Beispiel: Maximalwert von drei Zahlen

Im Sinne der testgetriebenen Softwareentwicklung zerlegen wir die Aufgabe, aus drei gegebenen Zahlen den Maximalwert zu ermitteln (in Anlehnung an [Kleuker 11]), in mehrere Iterationen. In einem ersten Iterationsschritt ermitteln wir den Maximalwert einer gegebenen Zahl max(x).

Testfall:

max(7) = 7

Code:

1. public int max (int x) {
2. int max=0; max=x;
3. return max;
4. }

In einer zweiten Iteration erweitern wir unsere Funktionalität, indem zwei gegebene Zahlen betrachtet werden. Die Testfälle und der Code werden entsprechend angepasst und erweitert.

Testfälle:

max(7,5) = 7

max(5,7) = 7

Code:

1. public int max (int x, int z) {
2. int max=0;
3. if x>z max=x;
4. else max=z;
5. return max;
6. }

In unserem letzten Iterationsschritt haben wir die geforderte Funktionalität erreicht und können den Maximalwert von drei Zahlen bestimmen. Wiederum werden zunächst die Testfälle und dann der Programmcode angepasst und erweitert.

Testfälle:

max(7, 5, 4) = 7

max(5, 7, 4) = 7

max(4, 5, 7) = 7

Code:

1. public int max (int x, int y, int z) {
2. int max=0;
3. if (x>z) max=x;
4. else if (y>x) max=y;
5. else if (z>y) max=z;
6. return max; }

Werden die drei definierten Testfälle zur Ausführung gebracht und anschließend ausgewertet, können wir erkennen, dass wir neben der 100 % Anweisungsüberdeckung sogar eine 100 % Zweig- bzw. Bedingungsüberdeckung (jede If-Bedingung wurde in den Tests mindestens einmal zu true und zu false ausgewertet) erreicht haben.

1. public int max
2. (int x=7,int y=5,int z=4){
3. int max=0;
4. if (7>4) max=7;
5. else if (5>7) max=y;
6. else if (4>5) max=z;
7. return 7;
8. }

1. public int max
2. (int x=5,int y=7,int z=4) {
3. int max=0;
4. if (5>4) max=5;
5. else if (7>5) max=7;
6. else if (z>y) max=z;
7. return 7;
8. }

1. public int max
2. (int x=4,int y=5,int z=7) {
3. int max=0;
4. if (4>7) max=x;
5. else if (5>4) max=5;
6. else if (7>5) max=7;
7. return 7;
8. }

Das erwartete Ergebnis (7) wird für alle drei Testfälle bei deren Durchführung bestätigt. Da eine 100 % Codeüberdeckung nachgewiesen ist, wird häufig keinerlei Veranlassung gesehen, weitere Tests durchzuführen.

Die aufmerksamen Leserinnen und Leser haben jedoch bestimmt schon erkannt, dass der entwickelte Programmcode nicht fehlerfrei ist. Zum Beispiel liefert der Testfall max(7,4,5) das fehlerhafte Ergebnis 5. Der durchgeführte Testfall max(7,5,4) hat allerdings das korrekte Ergebnis geliefert. Auch der Testfall max(7,7,7) gibt ein falsches Ergebnis (0) zurück. Wenn bei der Ermittlung der Testfälle neben dem Blick auf die Überdeckung eine systematische Herangehensweise (z. B. durch Einsatz von Testverfahren) genutzt wird, wird die Chance erhöht, die Fehler aufzuspüren. Wenn überlegt wird, dass nicht nur die Position der größten Zahl eine Rolle spielt, sondern auch die Reihenfolge der beiden anderen, dann ergeben sich folgende Konstellationen, die zu testen sind:

x > y > z und x > z > y

y > x > z und y > z > x

z > x > y und z > y > x

Damit hat sich die Anzahl der Testfälle zwar verdoppelt, aber der erste oben genannte Fehler im Programm wäre erkannt worden. Wird dann noch berücksichtigt, dass Zahlenwerte auch gleich sein können, ergeben sich die folgenden sieben weiteren Testfälle:

x = y > z und x = y < z

x = z > y und x = z < y

y = z > x und y = z < x

   sowie x = y = z

Mit dem letzten der nun insgesamt dreizehn Testfälle (und nicht nur die drei oben angegebenen) wird auch der zweite Fehler aufgedeckt. Weitere Testfälle (negative, große, kleine Zahlen, keine Zahlen usw.) sind ggf. ergänzend durchzuführen, je nachdem, wie die Spezifikation (design by contract) aussieht.

Leider wird in der agilen Welt das systematische Herangehen an die Herleitung der Testfälle als riesige und meist sogar gar nicht leistbare Aufgabe angesehen: „Using a testing technique, you would seek to exhaustively analyze the specification in question (and possibly the code) and devise tests that exhaustively cover the behavior“ ([Langr 13], Seite 35).

Im Beispiel konnten wir hoffentlich zeigen, dass die Komplexität der Lösungsschritte und damit auch der Testaufwand nicht linear steigen. 13 Testfälle bedeuten zwar mehr Aufwand bei deren Herleitung, diese beinhalten aber eine größere Bandbreite von Parameterkonstellationen. Der agile Entwickler hat mit den 13 dann fehlerfreien Testfällen ein „besseres Zutrauen“ zu seinem Programm als vorher mit den drei Testfällen. Zudem unterstützt die systematische Herleitung der Testfälle ein Stück weit dabei, die eigenen „blinden Flecke“ (blind spots) der Entwicklung zu überwinden. Beispiel: Größter gemeinsamer Teiler Als nächstes Beispiel wird die Ermittlung des größten gemeinsamen Teilers von zwei ganzen positiven Zahlen betrachtet. Das Beispiel wird nicht so ausführlich hergeleitet und dargestellt.

Testfälle:

gcd(4,6) = 2

gcd(6, 4) = 2

Code:

1. public int gcd(int m, int n) {
2.    // pre: m > 0 and n > 0
3.    // post: return > 0 and
4.    // m@pre.mod(return) = 0 and
5.    // forall(i:int | i > return implies
6.    // (m@pre.mod(i) > 0 or n@pre. mod(i) > 0)
7.    // …
8.    int r=0;
9.    if (n > m) {
10.      r = m;
11.      m = n;
12.      n = r;
13.    }
14.    r = m % n;
15.    while (r != 0) {
16.      m = n;
17.      n = r;
18.      r = m % n;
19.    }
20.    return n;
21. }

Mit der Ausführung des Testfalls gcd(4,6) werden bereits alle Anweisungen zur Ausführung gebracht. Beim Testfall gcd(6,4) wird die Bedingung (n>m) zu false ausgewertet und somit mit den beiden Testfällen 100 % Bedingungsüberdeckung erzielt. 100 % Pfadüberdeckung ist durch die vorhandene Schleife mit ihren beliebig vielen Schleifendurchläufen (und damit unterschiedlichen Pfaden) praktisch nicht erreichbar. Eine systematische Herangehensweise für den Test von Schleifen (Boundary-Interior-Pfadüberdeckungstest) ist gegeben, wenn mindestens je ein Testfall prüft,

  • dass die Schleife nicht ausgeführt wird (falls dies möglich ist, abweisende Schleife),
  • dass die Schleife nach einen einmaligen Durchlauf verlassen wird und
  • dass mindestens eine Wiederholung des Durchlaufs der Schleife ausgeführt wird.

Ein einmaliger Schleifendurchlauf ist bei den beiden oben angegebenen Testfällen gegeben. Bei systematischer Herangehensweise sind somit folgende ergänzende Testfälle erforderlich:

Herangehensweise sind somit folgende ergänzende Testfälle erforderlich:

gcd(4,4) = 4

(keine Ausführung der Schleife)

gcd(104,169) =13

(mehrere Schleifendurchläufe)

Die vier Testfälle erfüllen neben der Zweigüberdeckung auch eine systematische Prüfung der Schleife. Der für die Funktionalität wichtige Testfall, dass es keinen gemeinsamen Teiler > 1 gibt, ist noch nicht berücksichtigt. Durch Nutzung der Äquivalenzklassenbildung, einem systematischen Verfahren zur Herleitung von Testfällen (s. [Spillner 12]), wird dieser sehr wichtige Testfall nicht vergessen:

gcd(5,4)=1

Sicherlich sind auch bei diesem Beispiel noch weitere Testfälle sinnvoll. Werden diese mit systematischen Verfahren zum Testentwurf hergeleitet, dann ist der Test nicht erschöpfend, aber als ausreichend anzusehen. Die Systematik der Herleitung der Testfälle hilft dabei, dass keine wichtigen Tests vergessen werden. Und auch hier werden durch die ergänzenden Tests mögliche Lücken in der Funktionalität deutlich.

If you always follow the simple TDD cycle to drive your code, your coverage metric will approach 100 percent.” ([Langr 13], Seite 313)

Never set coverage numbers as goals. Managers who insist on high coverage numbers get what they ask for – high coverage numbers and little else useful.” ([Langr 13], Seite 314)

Die Beispiele zeigen sehr drastisch, dass eine 100%ige Codeüberdeckung keine Aussage über Fehlerfreiheit und damit Qualität zulässt. Es ist nur sichergestellt, dass alle Programmteile zur Ausführung gekommen sind bzw. jede Bedingung zu true und false ausgewertet wurde. Hier stimmen wir überein mit Bjørnvig, Coplien und Harrison: „TDD can give a false sense of confidence that the system is tested“ [Bjørnvig 07] und auch mit Grubers Aussage: „High code coverage is a desired attribute of a well tested system, but the goal is to have a fully and sufficiently tested system. Code coverage is indicative, but not proof, of a well-tested system“ [Gruber 08]. Erst mit der Erweiterung der einfachen Prinzipien von TDD durch eine systematische Herleitung von Testfällen (s. [Vosseberg 13]) kann bei deren Auswertung eine Aussage über die Qualität getroffen werden. Wir konnten hoffentlich zeigen, dass mit wenig mehr (kein erschöpfender) Aufwand ein großer Schritt in Richtung Qualität getan werden kann: Wir bezeichnen dies mit TDD++.

Literatur

[1] [Ambler] Scott W. Ambler: Introduction to Test Driven Development (TDD). http://www.agiledata.org/essays/tdd.html , 3.3.2014

[2] [Bjørnvig 07] Gertrud Bjørnvig, James O. Coplien, Neil Harrison: A Story about User Stories and Test Driven Development. Better Software 9 (11), November 2007, http://www.rbcs-us.com/images/documents/User-Stories-and-Test-Driven-Development.pdf, 3.3.2014

[3] [Gruber 08] Christian Gruber: Yet another misunderstanding of TDD, testing, and code coverage, November 2008 http://www.agileadvice.com/2008/11/04/uncategorized/yet-another-misunderstanding-of-tdd-testing-and-code-coverage/, 3.3.2014

[4] [it-agile AT] Agiles Testen. https://www.it-agile.de/wissen/praktiken/agiles-testen/, 3.3.2014

[5] [it-agile TDD] Was ist Testgetriebene Entwicklung? http://www.it-agile.de/wasisttdd.html, 3.3.2014

[6] [Kleuker 11]: Stephan Kleuker: Open Source Testwerkzeuge für alle Testphasen. 6. Treffen der User Group „Softwaretest und Qualitätssicherung“, Software Foren, 12. und 13.9.2011, Leipzig

[7] [Langr 13] Jeff Langr: Modern C++ Programming with Test-Driven Development: Code Better, Sleep Better, O‘Reilly Verlag GmbH & Co, November 2013

[8] [Martin 06] Robert C. Martin, Micah Martin: Agile Principles, Patterns, and Practices in C#. Prentice Hall, 2006

[9] [wiki TDD] Test Driven Development https://en.wikipedia.org/wiki/Test-driven_development, 3.3.2014

[10] [Spillner 12] Andreas Spillner, Tilo Linz: Basiswissen Softwaretest, 5. Auflage, dpunkt.verlag, Heidelberg, 2012

[11] [Vosseberg 13] Karin Vosseberg, Andreas Spillner: Traditionell cum Agil. Am Beispiel Test-Driven Development. SQMagazin, Juni 2013,

S. 32-35

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Kategorien

Recent Posts