Experience Embedded

Professionelle Schulungen, Beratung und Projektunterstützung

Test FAST statt Test FIRST beim Software-Unit-Test

Autoren: Dr. Stephan Grünfelder, Bernhard Peischl

Beitrag - Embedded Software Engineering Kongress 2015

 

Seit einigen Jahren sind Werkzeuge zur automatischen Erzeugung von Unit-Testfällen erhältlich. Diese erzeugen ohne jedes Wissen der korrekten Funktion der zu testenden Software Unit-Tests in kurzer Zeit. "Tests" werden alleine auf Basis des existierenden Quellcodes erzeugt. Das Testwerkzeug zielt dabei auf Erzielen einer hohen strukturellen Testabdeckung ab. Eine völlig andere Herangehensweise ist die Test-First-Strategie: Unit-Tests werden ausschließlich auf Basis der (Design-)Spezifikation erstellt, noch bevor der Code existiert. Eine strukturelle Testabdeckung ist dabei nicht entscheidend für den Test, eine hohe funktionale Testabdeckung ist gefragt. Welche Vor- und Nachteile diese beiden Strategien haben und wie man diese scheinbar unvereinbaren Strategien gewinnbringend verheiraten kann, zeigt dieser Artikel.

Test Driven Development (TDD), genannt auch Test-First-Strategie, ist eine Vorgehensweise aus dem Portfolio der agilen Software-Entwicklung. Es ist keine Testmethode sondern eine Entwicklungsmethode, bei der der Entwickler beginnt, den Unit-Test für den eigenen Code zu schreiben, bevor er mit dem Codieren beginnt. Zug um Zug werden in kleinen Iterationen zuerst der Test und dann der zu testende Code implementiert bzw. erweitert. Sobald der Test keine Fehler mehr meldet, beginnt eine neue Iteration.

Durch diese Entwicklungsmethode ist der Entwickler eines Software-Moduls gezwungen, das Modul-Design so zu gestalten, dass es an seinen Schnittstellen gut testbar ist. Weiters ist er gezwungen, seine Testfälle ausschließlich aus dem Design (der Spezifikation) des Moduls abzuleiten. Er ist nicht verführt, im Unit-Tests den Code bloß zu durchlaufen, also sich nur am zu testenden Quellcode zu orientieren, und dessen Zweck nicht genau zu hinterfragen. Im Gegenteil: Der Test testet den Code definitiv gegen das korrekte Verhalten, der zu testende Code liegt noch gar nicht vor.

TDD gegen traditionelles Unit-Testing

Im Gegensatz zum traditionellen Unit-Test, also dem Test nach der Programmierung, kann TDD nicht mit einem "Testendekriterium" aufwarten. In der agilen Literatur wird empfohlen, dann mit dem Schreiben von Testfällen aufzuhören, "wenn einem nichts mehr einfällt". Im klassischen Unit-Test ist üblicherweise das Erreichen einer gewissen strukturellen Testabdeckung eine notwendige Bedingung für das Testende. Häufig wird bei eingebetteten Systemen das Erreichen von 100% Decision Coverage, also das Durchlaufen aller Verzweigungen des zu testenden Codes, verlangt. Dabei sollte der Tester aber, wie beim TDD, die zu testende Einheit gegen ihren Zweck (ihre Design-Spezifikation) testen und zusätzlich dazu die Testabdeckung messen [1]. Wenn der Quellcode schon existiert, ist man aber ohne Zweifel dazu verführt, die Testfälle nur am Quellcode auszurichten und sich damit zufriedenzugeben, den Code bloß zu durchlaufen, anstatt ihn (gegen die Design-Spezifikation) zu testen. Doch für das Erstellen von "Testfällen", die den Code bloß durchlaufen, gibt es inzwischen moderne Unit-Test-Werkzeuge, die das in wenigen Sekunden automatisch fertig bringen: das Erzeugen von Testfällen auf Basis des Codes.

Automatische Erzeugung von Unit-Tests

Ein Test testet in der Regel für bestimmte Eingaben die Reaktion des Prüflings gegen eine erwartete Reaktion. Wenn wir von der automatischen Erzeugung von Testfällen sprechen, muss man einschränken, dass für gegebene Inputs das erwartete Ergebnis einer zu testenden Funktion einem Testwerkzeug nicht bekannt sein kann. Ein Testwerkzeug, das automatisch Testfälle für eine Funktion erzeugt, erzeugt daher nur Inputs für diese Funktion und kann für diese das tatsächliche Ergebnis berechnen und dieses als erwaretes Ergebnis vorschlagen.

Viele Werkzeuge gehen dabei so vor: sie suchen Testfälle aus einer typischerweise riesigen Anzahl von Möglichkeiten so aus, dass sie die strukturelle Testabdeckung maximieren. Viele Autoren nennen das dann Search-based Testing [7]. Werkzeuge aus dem akademischen Bereich, wie z.B. das Java-Werkzeug EvoSuite, verwenden für diese Suche unter anderem genetische Algorithmen [2]. Ein genetischer Algorithmus versucht die Prozesse der Evolution zu imitieren, allen voran die Idee des "survival of the fittest". Gut angepasst, also "fit", bedeutet in unserem Fall das Erreichen einer guten Testabdeckung. Eine initiale Population von Lösungskandidaten (in unserem Fall sind das Sets von Testfällen) wird durch Zufall oder mit Hilfe von heuristischen Methoden erzeugt. Die fittesten Paare dieser Kandidaten bringen gemeinsam Nachkommen hervor indem sie ihr Erbmaterial (ihre Testparameter) kombinieren. Dabei passiert mit einer gewissen Wahrscheinlichkeit crossing over oder eine Mutation. Mit jeder Iteration des Suchprozesses wird durch natürliche Selektion die fitness (die Testabdeckung) höher bis entweder einer der Kandidaten der Population eine Lösung ist (also das Testset 100% Testabdeckung erreicht) oder bis ein anderes Testende-Kriterium erfüllt ist, wie zum Beispiel das Überschreiten einer Zeitschranke.

Erforschung effizienter Suchverfahren

Pure genetische Such-Verfahren gehen bei ihrer Suche nicht gerade intelligent vor. Bis zum Beispiel ein String-Compare eine Übereinstimmug mit einem Passwort meldet, kann es tausende von Iterationen benötigen. Deshalb werden diese Verfahren auch manchmal durch Dynamic Symbolic Execution ersetzt oder ergänzt. Bei diesem intelligenteren Verfahren wird – vereinfacht gesagt – bei konkreten Testdurchläufen mitprotokolliert welche Parameter Verzweigungsbedingungen beeinflussen und diese dann für neue Testdurchläufe gezielt geändert. Das Werkzeug PEX verwendet Dynamic Symbolic Execution für den Test von .NET-Sprachen [3].

Aus akademischen Publikationen lässt sich schließen, dass Search-based Testing mehr und mehr an Bedeutung gewinnen könnte. Die Forschungsteams stellen aber in den seltensten Fällen Werkzeuge für Programmiersprachen der Hardware-nahen Programmierung vor. Eine Ausnahme ist da das Werkzeug AUSTIN (AUgmented Search-based TestINg), siehe https://code.google.com/p/austin-sbst. Auch dieses Werkzeug unterstützt sowohl Search-based-Testing als auch Dynamic Symbolic Execution und ist ein frei verfügbares Unit-Test-Werkzeug für C Programme und daher für Embedded-Entwickler besonders interessant. AUSTIN wurde sowohl im Automotive-Bereich [8] als auch zum Testen von Open-Source-Programmen eingesetzt [9]. Einer Studie am King’s College in London verglich AUSTIN mit anderen evolutionären Verfahren zur Testfallgenerierung und kam zum Schluss, dass AUSTIN bei der Generierung genauso effektiv hinsichtlich der strukturellen Abdeckung aber beträchtlich effizienter ist [8].

Kommerzielle Werkzeuge

Im Gegensatz zu den Werkzeugen akademischen Ursprungs ist bei kommerziellen Werkzeugen natürlich wenig bekannt, wie sie arbeiten. Für Embedded Software Testing sind diese Werkzeuge aber gerade am interessantesten, weil sie Tests für C/C++ erzeugen. Das Unit-Test-Werkzeug Cantata zum Beispiel dürfte zur Suche von Testfällen eine Art von Backtracking verwenden. Beim Backtracking spielt der Zufall eine deutlich geringere Rolle. Wie bei der Suche eines Auswegs aus einem Labyrinth bewerten diese Algorithmen die momentane Situation immer wieder neu und laufen gegebenenfalls gezielt zu einem früher schon besuchten Ort zurück um dort einen anderen Lösungsweg zu suchen. Als Startpunkt für diese Suche dürfte Cantata für Umgebungswerte und Parameter der zu testenden Funktionen einfach Null wählen, wie die für Listing 1 durch dieses Werkzeug erzeugten Testfälle vermuten lassen.

#include <malloc.h>

#include <stdio.h>

 

static int* values;

static int size = 0;

static int max_size = 3;

 

void init_stack()

{

   values = malloc(3 * sizeof(int));

}

 

static void resize()

{

   values = realloc(values, max_size * 2);

   max_size = max_size * 2;

}

 

void push(int x)

{

   if (size >= max_size) resize(); /* full stack */

   if (size < max_size)

   {

      values[size++] = x;

   }

   /* else branch is infeasible, thus 100% DC is infeasible */

}

 

int pop()

{

   if (size > 0)

   {

      return values[size--];

   }

   else

   {

      printf("error");

      return 0;

   }

}

 

Listing 1: Eine Stack-Implementierung aus [4] entnommen und von Java in C übersetzt.

Listing 1 zeigt eine besondere Herausforderung für ein Testwerkzeug. Für diesen Code gibt es eigentlich keine Testfälle, die 100% Verzweigungsabdeckung erreichen können, denn für die Funktion push() kann niemals der zweite else-Zweig erreicht werden. Das Werkzeug EvoSuite versucht durch wiederholge Aufrufe der angebotenen Funktionen die 100% Verzweigungsabeckung zu erreichen und löst das Problem schließlich indem es ab einer gewissen Zeit akzeptiert bestimmte Pfade nicht betreten zu können [4]. Cantata hat eine "brutalere" Lösung, die es gestattet, dennoch 100% Decision Coverage für Listing 1 zu erreichen: die Variablen max_size und size werden vor dem Aufruf von push() mit Null überschrieben. Mit den Default-Settings eines Projekts ist es dem Werkzeug erlaubt auch static-Variablen von außen zu manipulieren und so ist 100% Verzweigungsabdeckung für push() erreichbar. Der dabei durchlaufene Pfad kann aber im Produktivsystem nie durchlaufen werden, weil max_size nie Null werden kann. Abbildung 1 (siehe PDF) zeigt wie Cantata die 6 automatisch erzeugten Testfälle präsentiert und Abbildung 2 (siehe PDF) zeigt, wie der Benutzer in einem Summary Report über die erzielte Testabdeckung informiert wird.

Akademische Werkzeuge

Forschungsteams statten manche akademische Werkzeugen noch mit Produktfeatures aus, die bei kommerziellen Unit-Test-Werkzeugen für Embedded Systems (noch) nicht zu finden sind und vielleicht auch in Zukunft garnicht wirklich gefragt sind: sie testen den Code gegen allgemeingültige Anforderungen: kein Überlauf von arithmetischen Operationen, keine Nullpointerdereferenzierung, keine Division durch Null, Arraysindizes müssen im definierten Wertebereich sein.

Die Abwesenheit einer der beschriebenen Fehler kann in einem Test natürlich nicht nachgewiesen werden, aber mann kann ja einmal danach suchen, ob es gelingt so einen Fehler zu provozieren. Um dem Such-Algorithmus das Provozieren eines Array-Indexüberlaufs schmackhaft zu machen, transformiert das Werkzeug Evosuite im Java-Bytecode Zugriffe auf Arrays in der folgenden Art:

void test(int x)

{

   if (x < 0) throw new NegativeArraySizeException();

   if (x >= foo.length) throw new ArrayIndexOutOfBoundsException();

   foo[x] = 0;

}

 

Listing 2: Demo der Von EvoSuite verwendeten Code-Transfomation, siehe [3].

Hier werden – im Gegensatz zu den bisher diskutierten generierten Testfällen – mit hoher Wahrscheinlichkeit tatsächlich Testfälle erzeugt, die darauf abzielen die Einhaltung einer Anforderung der Spezifikation abzutesten: die Software soll nicht abstürzen. Im Embedded-Bereich verzichtet man aber absichtlich manchmal um der Performance Willen auf robusten Code und verbietet z.B. Überläufe nicht, wenn dem Umfeld der Applikation diese Überläufe egal sind oder diese extrem unwahrscheinlich sind. Das heißt: viele, aber nicht alle auf diese Art automatisch erzeugten Tests machen in der Praxis auch wirklich Sinn.

Fehlerfindung mit und ohne Werkzeug, Teil 1

An der Universität Sheffield gab es einen groß angelegten Versuch mit über 100 Probanden mit dem Ziel herauszufinden, ob man bei Verwendung durch das Werkzeug EvoSuite automatisch erzeugter Unit-Tests mehr Fehler findet als bei händischer Implementierung von Tests mit JUnit [5]. Die Probanden waren großteils Studenten. In der Studie wurden ihnen 2 bzw. 3 Stunden für eine Testaufgabe mit und ohne Testgenerator gegeben und kurz vor Erhalt der Aufgabe gab es einen Auffrischungskurs für JUnit. Die Aufgabensteller bemühten sich um möglichst objektive Ergebnisse bei diesem Vergleich:

  • Die Kommentare im Quellcode waren ausführlich genug um als vollständige Spezifikation dienen zu können.
  • Die zu testenden Java-Klassen waren weder trivial noch so komplex, dass spezielles Fachwissen nötig/hilfreich war.
  • Die zu testenden Klassen sollten ohne zeitaufwändiges Studium anderer Klassen verständlich sein und hatten einen Mix aus String-Verarbeitung und numerischen Aufgaben.

Ein Ergebnis des Versuchs war, dass man herausfand, dass die erreichte Testabdeckung mit und ohne Werkzeug etwa gleich hoch war – die Probanden hatten keine Möglichkeit die erzielte Testabdeckung zu messen. Das war recht erfreulich für die Wissenschafter, zeigte es doch, dass sich ihr Testgenerator mit meschlichen Gegenspielern messen konnte. Ergebnis Nummer Zwei war aber für manche Personen ernüchternd: auch die Quote an gefundenen Fehlern war mit und ohne Testgenerator etwa gleich hoch.

A fool with a tool is still a fool

Die Autoren des Experiments merkten bei der Publikation ihrer Ergebnisse korrekterweise kritisch an, dass die Probanden den Code vor dem Versuch nicht kannten. Das ist eine Situation, die für die wenigsten Projekte zutrifft, denn fast immer ist der Autor von Unit-Tests der Autor des Codes. Aber da darf man wohl noch ein wenig weitere Kritik hinzufügen:

  • Für ein Industrieprojekt ist nicht nur die mit einem Werkzeug erzielte Testtiefe/Qualität interessant sondern auch die für die erreichte Qualität anfallenden Kosten. Also ein Performance-Vergleich der beiden Gruppen wäre wohl eine der interessantesten Zahlen für die industrielle Anwendung.
  • Dazu müsste man korrekterweise Probanden vergleichen, die über monatelange Erfahrung im Einsatz der jeweiligen Technik verfügen. Zu so einem Vergleich wird es vermutlich aus Aufwandsgründen nie kommen.
  • Viele (vergleichsweise unerfahrene) Probanden verbrachten (verschwendeten) einen Teil ihrer Zeit damit herauszufinden, wie die automatisch generierten Tests das Testobjekt durchlaufen anstatt anhand der Testdaten zu beurteilen, ob der automatisch erzeugte Test Sinn macht oder nicht, und die Zeit für das händische Erstellen weiterer Testfälle zu nützen.

Der letzte Kritikpunkt ist eine mögliche Erklärung der gleichen Feherquote mit und ohne Testgenerator: Fehler werden durch durchdachte und schlagkräftige Testfälle gefunden. Dazu verhelfen einschlägige Weiterbildung, Geschick und Erfahrung des Testers. Das Testwerkzeug kann dem Tester das Denken nicht abnehmen und wird daher die Anzahl der gefundenen Fehler nicht erhöhen. Sehr wohl kann es aber die Testerstellung beschleunigen, weil es dem Tester vergleichsweise stupide Arbeit abnimmt und mehr Zeit für die Konzentration auf die Teststrategie lässt, wie die nächsten Absätze des Artikels diskutieren.

Fehlerfindung mit und ohne Werkzeug, Teil 2

Auch Unit-Test-Werkzeuge ohne automatische Testerstellung beschleunigen das Schreiben von Tests. So werden zum Beispiel Stubs (Mockups, Rümpfe des vom zu testenden Codees aufgerufenen aber nicht verfügbaren Funktionen) automatisch erzeugt und Testtreiber auf Basis von tabellarischen Eingaben durch das Werkzeug erstellt. Diese Beschleunigung im Vergleich zur Test-First-Strategie ist möglich, weil das Werkzeug die Schnittstellen des zu testenden Codes analysieren kann und dem Benutzer damit Tipp-Arbeit abnehmen kann.

Testwerkzeuge wie Cantata, Tessy, VectorCast und andere protokollieren auch auf Wunsch die strukturelle Testabdeckunng. Wie oben erwähnt ist das Erreichen von 100% der gewünschten Abdeckung zwar keine ausreichende Bedingung das Testen zu beenden, doch ist das Nicht-Erreichen von 100% fast immer ein Zeichen, dass noch Testfälle fehlen. Auf diese Alarmanlage verzichtet die Test-First-Strategie.

Und noch eine Unterstützung wird zumindest von Cantata zur Verfügung gestellt, die bei TDD nicht möglich ist: das Testwerkzeug gibt dem Tester in gewissem Umfang nicht nur die Möglichkeit festzustellen, dass der zu testende Code das tut was er soll (durch Definition der Testfälle), sondern auch festzustellen, wenn etwas passiert, was nicht passieren soll: Cantata überprüft nach jedem Testfall alle Variablen auf Veränderung und kann somit z.B. feststellen, wenn eine verirrte Zeigervariable unbeabsichtigt andere Variablen überschreibt.

Wenn sich diese drei Vorteile des Einsatzes eines zeitgemäßen Unit-Test-Werkzeugs im Vergleich zu TDD sich auch ohne automatische Testerzeugung auswirken und wenn das Experiment der Universität Sheffield gezeigt hat, dass man mit Testgeneratoren auch nicht mehr Fehler findet als ohne: was bleibt dann von der automatischen Testerzeugung? Die Teschbeschleunigung. Die nächsten Absätze zeigen wie man die zentralen Ideen von TDD im Test mit Testgeneratoren anwendet, dadurch eine weitere Verkürzung der Testszeit erfährt und somit Unit-Tests profitabler werden lässt.

Test FAST statt Test FIRST

Am Anfang des Artikels wurden Vorteile von TDD diskutiert. Im vorhergehenden Abschnitt wurden Nachteile von TDD gegenüber dem Einsatz von modernen Unit-Test-Werkzeugen erläutert. Nun zeigen wir wie man die Vorteile beider Ansätze kombinieren und nutzen kann und dabei die jeweiligen Nachteile überdeckt.

Ohne Zweifel können Vorteile und die zentralen Ideen von TDD, (1) die der ausschließlichen Orientierung an der Spezifikation bei der Definition der Testfälle und (2) das Design von Units, die an ihren Schnittstellen gut testbar sind, auch im klassischen Test mit Werkzeug Anwendung finden. Dazu benötigt es nur einer gewissen Disziplin.

Auch sollte man Testgeneratoren nicht überschätzen und sich nicht von ihnen in die Irre leiten lassen. Tests aus dem Generator können völlig unsinnig sein. Das überlegte Testdesign ist und bleibt Aufgabe des Testers. Doofe Tipparbeit kann durch den Generator reduziert werden, nicht das ideenreiche Testdesign. Wenn man also automatische Unit-Test-Erzeugung gewinnbringend einsetzen will, dann muss man wissen wie man die Werkzeuge einsatzt. Der folgende „TEST Fast“ genannte „Verhaltenskodex im Umgang mit Testgeneratoren“ kann dabei helfen das Beste aus einem Unit-Test-Projekt herauszuholen und im Vergleich zu anderen Vorgehensweisen Testzeit zu sparen. Dazu ein kleiner Appetitanreger: Die automatische Erstellung der Tests zu Listing 1 benötigt weniger als 2 Sekunden.

Zur Umsetzung von Test FAST wird das Beachten der folgende Regeln empfohlen:

  • Denken Sie schon beim Design von neuem Code an seine Testbarkeit.
  • Lassen Sie das Testwerkzeug Testfälle generieren un bewerten Sie die Tauglichkeit dieser Testfälle ohne Blick auf den Quellcode. Die Referenz ist alleine die Design-Spezifikation. Streichen Sie alle Testfälle die keine Relevanz haben.
  • Ergänzen Sie nun eigene Testfälle, so unmittelbar nach dem Schreiben des Codes, wie sinnvoll und möglich. Entwerfen Sie diese Testfälle ausschließlich auf Basis der Design-Spezifikation, vermeiden Sie den Blick in den Quellcode.
  • Verwenden Sie methodische Black-Box-Testverfahren, wie Grenzwertanalyse, Paarweiser Test, Entscheidungstabellentechnik und so weiter zum Herleiten von schlagkräftigen Testfällen.
  • Messen Sie die Testabdeckung erst, wenn Sie denken, dass die Unit Under Test vollständig getestet ist. Wenn die Test unerwartet noch nicht 100% der Abdeckung erreichen, versuchen Sie zuerst durch Analyse der Testfälle eine höhere Abdeckung zu erreichen, ehe Sie den Test debuggen und den zu testenden Code inspizieren.

So manche Firmen habe versucht Test First einzuführen um Qualitätsverbesserungen zu erfahren. Viele davon haben es wieder aufgegeben, weil die Programmierer es nicht besonders spannend fanden zuerst (auch banale) Testfälle zu schreiben die sie später vielleicht wieder wegschmeißen mussten wenn sie im Zuge der Programmierung die Architektur einer Software-Unit verändert haben. Test FAST hat das Potenzial hier gegenzusteuern: die Programmierer können sich zuerst auf die Implementierung des Produkts, dann auf die Implementierung der knackigen Testfälle konzentrieren, die einfachen Tests werden automatisch erzeugt.

Zusammenfassung und Ausblick

In diesem Artikel wurde gezeigt wie Unit-Test-Generatoren funktionieren und auf welche Art man sie gewinnbringend einsetzt. Dabei wurden unbestrittene Vorteile von TDD auf den Werkzeugeinsatz transportiert und das Vorgehensmodell Test FAST vorgestellt.

Im Artikel wurde darauf hingewiesen, dass für automatische wie manuell erstellte Tests das Erreichen von 100% Testabdeckung zwar meist eine notwendige Bedingung für gute Tests ist, aber ganz gewiss nicht hinreichend. Listing 3 zeigt ein weiteres Beispiel. Die Testfälle erreichen 100% Verzweigungsabdeckung und finden dennoch einen ganz gravierenden Fehler nicht.

unsigned max(unsigned a, unsigned b, unsigned c)
{

   unsigned max = 0;

   if (a > c) max = a;
   if (b > a) max = b;
   if (c > b) max = c;

   return max;

}

 

/*

     Test 1: max(3,5,7) == 7

     Test 2: max(5,7,3) == 7

     Test 3: max(7,5,3) == 7

*/

 

Listing 3: Ein Beispiel von schlechten Tests aus [6]. Ein Bug wird nicht erkannt.

Auch die meisten momentan am Markt erhältlichen kommerziellen Werkzeuge zur automatischen Erzeugung von C/C++ Unit-Tests haben zur Zeit für Listing 3 keine Testfälle anzubieten, die den Benutzer auf den Fehler aufmerksam machen, weil sie nur die Testabdeckung maximieren aber noch keine intelligente Wahl von Testdaten anbieten. Die Hersteller arbeiten aber stets daran die Testgeneratoren intelligenter zu machen. Für das Jahr 2016 ist zu erwarten, dass viele kommerzielle Generatoren intelligent genug sind, dass man für Listing 3 folgende Testvorschläge präsentiert bekommt und auf diese Art schnell den Fehler des Codes erkennt:

  • max(INT_MAX, 0 ,0) == INT_MAX
  • max(0, INT_MAX ,0) == INT_MAX
  • max(0, 0, INT_MAX) == INT_MAX
  • max(INT_MAX, INT_MAX , INT_MAX) == 0

Fachliches Nachwort

Eine Methode, die sich ausschließlich am Quellcode orientiert aber für die es kaum Werkzeug-Unterstützung gibt, ist das Baseline Testing – oft illustriert mit Hilfe von Kontrollflussgraphen. Die Idee dieses Verfahrens ist einen Satz von unabhängigen Pfaden durch den Kontrollflussgraphen der zu testenden Software zu erzeugen. Dabei wird, vereinfacht gesagt, ein neuer Testfall aus bestehenden erzeugt, indem immer nur eine einzige Richtungs-Entscheidung verändert wird. So lange, bis das nicht mehr möglich ist.

int x = 0;

 

int tu_nichts(int a, int b)

{

   if (a > 10) x += 47;

   if (b > 10) x -= 47;

   return x;

}

 

Listing 4: Dieser Code sollte x niemals verändern. Tut er aber.

Beispielsweise könnte man mit diesem Verfahren für den Code aus Listing 4 die folgenden drei Testfälle ableiten.

  • einen Fall, bei dem keine der beiden Bedingungen zutreffen, z.B. foo(10,10);
  • einen Fall, bei dem nur die erste Bedingung wahr ist, z.B. foo(11,10);
  • einen Fall, bei dem nur die zweite Bedingung wahr ist, z.B. foo(10,11).

 

Um 100% Decision Coverage zu erreichen wären hier zwei Testfälle ausreichend. Die beiden Testfälle foo(11,11)und foo(10,10) durchlaufen die beiden Entscheidungen in jeweils jede mögliche Richtung, erkennen aber nicht, dass x unerlaubter Weise verändert wird. Mit Hilfe von Baseline Testing hätte man den Fehler in der Funktion max() vergleichsweise leicht gefunden, außer der Pfad, der alle drei if-Bedinungen nicht feuern lässt, wird durch den Aufruf max(0,0,0) betreten. Das ist die einzige Wahl von Parametern, die den Fehler beim Baseline-Testing nicht findet. Baseline-Testing wählt die Testfälle anhand des Quellcodes zwar intelligenter, ist aber bei ungeschickter Wahl der Testdaten auch nicht schlagkräftiger.

Literatur

Die Publikationen von Gordon Fraser et al kann man von www.evosuite.org kostenfrei herunterladen.

[1] Stephan Grünfelder, Software-Test für Embedded Systems, Dpunkt-Verlag, Heidelberg 2013.

[2] Gordon Fraser, Andrea Arcuri, Phil McMinn: A Memetic Algorithm for Whole Test Suite Generation. Journal of Systems and Software, Volume 103, May 2015, pages 311–327.

[3] Gordon Fraser, Andrea Arcuri: 1600 Faults in 100 Projects: Automatically Finding Faults While Achieving High Coverage with EvoSuite. Empirical Software Engineering, June 2015, Volume 20, Issue 3, pp 611-639.

[4] Gordon Fraser, Andrea Arcuri: Evolutionary Generation of Whole Test Suites. Proc. 11th International Conference on Quality Software, 2011, pp. 31-40.

[5] G. Fraser, M. Staats, P. McMinn, A. Arcuri, F. Padberg: Does Automated Unit Test Generation Really Help Software Testers? A Controlled Empirical Study. ACM Transactions on Software Engineering Methodology, vol. 24, no. 4, 2015.

[6] Andreas Spillner, "Agilität und systematischer Test", Vortrag beim BCD Acceptance Café am 16. April 2013 in Wien.

[7] Ali, S.; Briand, L.C.; Hemmati, H.; Panesar-Walawege, R.K., "A Systematic Review of the Application and Empirical Investigation of Search-Based Test Case Generation," in Software Engineering, IEEE Transactions on , vol.36, no.6, pp.742-762, Nov.-Dec. 2010.

[8] Lakhotia, K.; Harman, M.; Gross, H., AUSTIN: A Tool for Search Based Software Testing for the C Language and Its Evaluation on Deployed Automotive Systems, in Search Based Software Engineering (SSBSE), 2010 Second International Symposium on , vol., no., pp.101-110, 7-9 Sept. 2010.

[9] K. Lakhotia, P. McMinn, and M. Harman, Automated Test Data Generation for Coverage: Haven’t We Solved This Problem Yet? in 4th Testing Academia and Industry Conference - Practice and Research Techniques, 2009, pp. 95–104.

[10] P. McMinn, Search-based software test data generation: A survey, Software Testing, Verification and Reliability, vol. 14, no. 2, pp. 105–156, Jun. 2004.

 

Beitrag als PDF downloaden


Test, Qualität & Debug - unsere Trainings & Coachings

Wollen Sie sich auf den aktuellen Stand der Technik bringen?

Dann informieren Sie sich hier zu Schulungen/ Seminaren/ Trainings/ Workshops und individuellen Coachings von MircoConsult zum Thema Test, Qualität & Debug.

 

Training & Coaching zu den weiteren Themen unseren Portfolios finden Sie hier.


Test, Qualität & Debug - Fachwissen

Wertvolles Fachwissen zum Thema Test, Qualität & Debug steht hier für Sie zum kostenfreien Download bereit.

Zu den Fachinformationen

 
Fachwissen zu weiteren Themen unseren Portfolios finden Sie hier.