20 Unit-Test |
Dieses Kapitel behandelt die folgenden Themen:
Unit-Test
Werkzeuge
Boost Unit Test Framework
Das Testen von Software ist aufwendig: Es nimmt typischerweise etwa ein Drittel des gesamten Softwareentwicklungsaufwands ein. Der Anteil ist bei sicherheitskritischen Systemen noch erheblich höher. Es gibt verschiedene Arten von Tests: Modul(Unit)-Test, Integrationstest, Systemtest, Abnahmetest. So dient der Abnahmetest dazu, die Funktionalität des Systems bzw. der Software dem Auftraggeber gegenüber nachzuweisen. Ein Bestehen dieses Tests ist in der Regel vertragliche Voraussetzung für die Bezahlung des Werks. Testen hat zwei Aufgaben:
Fehler finden: Je früher ein Fehler erkannt und beseitigt wird, desto besser. Wenn Sie testen, legen Sie sich eine sehr kritische Haltung gegenüber dem zu testenden Prüfling zu! Seien Sie geradezu »sadistisch« und versuchen Sie, das Programm mit Ihren Testfällen möglichst zum Absturz und zu falschen Ergebnissen zu bringen. Unbewusst oder bewusst Schwächen eines Programms auszublenden, spart zwar kurzfristig Zeit, verursacht aber langfristig Ärger.
Qualität nachweisen: Qualität ist der Grad, in dem ein System bzw. eine Software Anforderungen erfüllt. Bei dem oben genannten Abnahmetest liegen die Anforderungen in schriftlich festgelegter Form vor (Spezifikation/Pflichtenheft). Es geht dabei nicht nur um funktionale Anforderungen (etwa korrekte Berechnung), sondern auch um nicht-funktionale, zum Beispiel Berechnung innerhalb einer maximalen Zeitdauer, Robustheit gegen fehlerhafte Daten usw.
Zum Testen von C++-Programmen finden Sie in [SpiBr] viele Hinweise. Am naheliegendsten ist der Unit-Test. Dabei testet man die selbstgeschriebene Einheit (englisch unit), bevor sie weitergegeben wird. Der Vorteil: Man kennt die Software genau – und kann deshalb leichter als andere Fehler finden. Der Nachteil: Man kennt die Software genau – und ist deswegen »betriebsblind«, das heißt, das eigene Produkt wird zu unkritisch gesehen. Es mag unbewusste emotionale Widerstände geben, weswegen Tests auf höherer Ebene nicht von denselben Personen geplant und ausgeführt werden sollen, die die Software entwickelt haben. Erst nach bestandenem Unit-Test ist der Programmteil bereit zur Integration mit anderen Komponenten. Der Integrationstest dient zur Prüfung, ob alle integrierten Komponenten wie beabsichtigt zusammenspielen.
20.1 | Werkzeuge |
Ein Programm mal eben auszuprobieren, ist kein Testen. Testfälle müssen systematisch mithilfe von Testverfahren spezifiziert werden, zum Beispiel Äquivalenzklassenbildung. Darunter versteht man die Aufteilung von Tests in Klassen, die äquivalent sind, also Gemeinsamkeiten haben. Ein Beispiel für den Test eines Sortierprogramms: Die Testfolgen »1, 3, 7, 4, 5, 8« und »9, 2, 22, 6, 1« gehören zur Äquivalenzklasse »alle Zahlen sind unterschiedlich«, im Gegensatz zur Folge »100, 100, 4, 40, 9, 7«, die mindestens zwei gleiche Zahlen enthält. Zum Finden des Fehlers im Sortierprogramm der Aufgabe von Seite 166 ist gerade dieser Unterschied wichtig. Äquivalenzklassen reduzieren die Anzahl von Tests in den Fällen, in denen die Zahl aller möglichen Testfälle einfach zu groß ist, um noch vernünftig handhabbar zu sein. Schon zwanzig voneinander unabhängige if-Anweisungen können sich zu über einer Million (genau: 220) verschiedener Möglichkeiten der Ausführung eines Programms addieren. Oder, um das Beispiel des Sortierprogramms wieder aufzugreifen: Wenn alle möglichen Kombinationen einer Folge von N Werten getestet werden sollen, ist der Test bereits bei einem kleinen N (zum Beispiel 20) nicht mehr durchführbar.
Die notwendige Anzahl von Testfällen kann also recht groß werden, schon bei Software mittlerer Größe. Weil nach einer Änderung der Software möglicherweise auch entfernte Softwareteile betroffen sind, müssen alle Tests wiederholt werden (Regressionstest). Das lässt sich nur dann ökonomisch bewerkstelligen, wenn der Testprozess automatisiert abläuft.
Der Test selbst ist ebenfalls Software, die programmiert werden muss. Da wesentliche Elemente des Testens wiederkehren, ist es sehr empfehlenswert, Frameworks für die Programmierung der Tests einzusetzen und auf eine rein individuelle Lösung zu verzichten. Für diese Frameworks hat sich der Name XUnit eingebürgert, in Anlehnung an JUnit, ein Unit-Test-Framework für die Programmiersprache Java. Dabei steht X für den Kontext, zum Beispiel DB für ein Framework zum Testen von Datenbankanwendungen. Es gibt mehrere Frameworks für Unit-Tests in C++. Ich stelle Ihnen in Auszügen das Boost Unit Test Framework vor, nicht nur, weil die Boost-Libraries den C++-Standard beeinflusst haben, sondern auch, weil sie portabel und für ihre Qualität bekannt sind. Außerdem verwende ich in diesem Buch die Boost Libraries bei der Internet-Anbindung und einigen Algorithmen und sehe keinen Anlass zu wechseln. Dennoch möchte ich auf ein weiteres bekanntes Werkzeug zum Unit-Test hinweisen: googletest, das Google C++ Testing Framework (https://github.com/google/googletest). Unter anderem wird dieses Werkzeug in [SpiBr] verwendet. Der Hinweis soll keine Wertung darstellen, auch gibt es noch weitere Werkzeuge, die hier nicht aufgeführt werden. Sie arbeiten alle nach einem ähnlichen Schema.
20.2 | Boost Unit Test Framework |
Das Boost Unit Test Framework stellt Komponenten zur Verfügung, die das Schreiben von Tests, die Organisation von Tests und den kontrollierten Ablauf erlauben. Obwohl das Framework aus vielen Klassen besteht, ist die einfachste Schnittstelle zur Benutzung ein Satz von Makros. Mit wenigen Ausnahmen werde ich mich im Folgenden darauf beschränken, weil mit den Makros die wichtigsten Bereiche abgedeckt werden. Allen, die mehr wissen möchten, empfehle ich [Roz].
Es wird davon ausgegangen, dass die Boost-Libraries installiert sind. Die Optionen sind:
Statisches Linken der Boost-Test-Library
Dynamisches Linken der Boost-Test-Library
»Header-only«: Damit ist gemeint, dass an einer Stelle die Quelltexte aller benötigten Funktionen mit einer #include-Anweisung eingebunden werden. Der Nachteil: Die Compilation dauert länger. Der Vorteil: Beim Linken muss keine Bibliothek angegeben werden. Alle Funktionen werden statisch zur ausführbaren Datei gebunden.
»Auto-Linking«: eine spezielle Möglichkeit des Frameworks für Microsoft Compiler, die zu linkenden Teile automatisch zu ermitteln.
Der Einfachheit halber wird in den Beispielen die »Header-only«-Variante gewählt. Es genügt, im Shell-Fenster make einzugeben, um alles zu übersetzen und zu linken.
Eine Test-Suite besteht aus einer Menge von Testfällen. Das Makro BOOST_AUTO_TEST_SUITE ( suite_name ) startet eine Test-Suite, mit BOOST_AUTO_TEST_SUITE_END() wird sie beendet. Jedes BOOST_AUTO_TEST_CASE-Makro fügt einen Testfall hinzu. Wenn BOOST_TEST_MAIN definiert ist, wird automatisch eine main()-Funktion erzeugt. Das folgende Beispiel zeigt eine Test-Suite mit zwei einfachen Testfällen. Im ersten wird die Funktion length() der Klasse string geprüft, im zweiten ihr Gleichheitsoperator. BOOST_CHECK(bedingung) prüft die Bedingung und gibt eine Fehlermeldung aus, wenn die Bedingung nicht erfüllt ist.
#define BOOST_TEST_MAIN #include <boost/test/included/unit_test.hpp> BOOST_AUTO_TEST_SUITE(einfacher_stringtest) BOOST_AUTO_TEST_CASE(laenge) { std::string s("xyz"); BOOST_CHECK(s.length() == 4); } BOOST_AUTO_TEST_CASE(gleichheits_operator) { std::string_view s("abc"); BOOST_CHECK(s == "abc"); } BOOST_AUTO_TEST_SUITE_END()
Weil main() automatisch erzeugt wird, ist die Test-Suite nach Compilation lauffähig. Die Ausgabe des Programms ist
Falls nun fälschlicherweise 4 statt 3 im ersten Test eingetragen wird, ist die Ausgabe
Die Zahl in Klammern gibt die Zeilennummer der Testdatei an, in der der Fehler auftrat. Wie Sie sehen, bedeutet ein Fehlschlagen des Tests nicht unbedingt, dass der Prüfling einen Fehler hat – es kann auch der Test falsch sein. Die Wurzel des Testfallbaums ist die »Master Test Suite«, die mehrere Test-Suiten enthalten kann. Der Name kann geändert werden, wenn statt BOOST_TEST_MAIN zum Beispiel BOOST_TEST_MODULE neuer_name geschrieben wird. Die Prüfung einer einfachen Bedingung gibt es in drei Varianten:
BOOST_WARN(Bedingung): Die Nichterfüllung der Bedingung wird nicht als Fehler gesehen und auch nicht als solcher gezählt. Bei der Standardeinstellung gibt es keine Meldung, erst wenn die ausführbare Datei mit einem passenden Log-Level aufgerufen wird, zum Beispiel testprog.exe --log_level=warning. Das Testprogramm läuft weiter.
BOOST_CHECK(Bedingung): Die Nichterfüllung der Bedingung wird als Fehler gesehen und gemeldet. Das Testprogramm läuft weiter.
BOOST_REQUIRE(Bedingung): Die Nichterfüllung der Bedingung führt zur Ausgabe »fatal error«. Solche Fehler werden als so kritisch angesehen, dass eine Fortsetzung des Tests nicht sinnvoll ist.
20.2.1 | Fixture |
Im JUnit-Test-Sprachgebrauch bezeichnet »Fixture« durchzuführende vorbereitende Maßnahmen, mit setUp() aufgerufen, und nachbereitende Maßnahmen (Aufräumarbeiten), mit dem Namen tearDown() verbunden. Diese Maßnahmen können zum Beispiel dafür sorgen, dass vor jedem Testfall dieselben Bedingungen herrschen. Die Durchführung eines Testfalls besteht aus vier Phasen:
Vorbereitende Maßnahmen (setUp)
Test durchführen
Ergebnis prüfen und protokollieren
Aufräumarbeiten (tearDown)
Der Vorteil der Verwendung von Fixtures besteht in der Trennung der vor- und nachbereitenden Maßnahmen vom eigentlichen Test. Das C++-Prinzip »Resource Acquisition Is Initialization« (RAII, siehe Glossar) erlaubt es, auf die Methoden setUp() und tearDown() zu verzichten. An ihre Stelle treten Konstruktor und Destruktor. Ein einfaches Beispiel: Wenn ein Datum-Objekt dynamisch angelegt werden soll, lässt sich das leicht mit einem unique_ptr<Datum> als Fixture realisieren:
BOOST_AUTO_TEST_CASE( konstruktor_mit_fixture ) { std::unique_ptr<Datum> p = std::make_unique<Datum>(1, 1, 2026); // setUp BOOST_CHECK( p->jahr() == 2026 ); } // delete automatisch durch unique_ptr-Destruktor (tearDown)
Im folgenden Abschnitt wird ein Fixture auf globaler Ebene zum Öffnen und Schließen einer Log-Datei eingesetzt.
20.2.2 | Testprotokoll und Log-Level |
Es gibt zwei Möglichkeiten, eine Datei als Testprotokoll zu erzeugen. Die erste ist, die Bildschirmausgabe in eine Datei umzuleiten. Dabei kann der Log-Level als Parameter übergeben werden. Beispiel:
Anstelle der Übergabe in der Kommandozeile kann dasselbe mit einer entsprechenden Definition der Umgebungsvariable BOOST_TEST_LOG_LEVEL erreicht werden. Die Spalte eins der Tabelle 20.1 enthält weitere Werte für den Log-Level.
Die zweite Möglichkeit ist, per Programm eine Log-Datei anzulegen und den Log-Level zu definieren. Dazu wird eine Klasse mit den entsprechenden Einstellungen definiert:
class LogKonfiguration { public: LogKonfiguration() : test_log("test.log") // Dateiname { boost::unit_test::unit_test_log.set_stream(test_log); boost::unit_test::unit_test_log.set_threshold_level( boost::unit_test::log_test_units); // Log-Level } ~LogKonfiguration() // schließen und zurücksetzen { test_log.close(); boost::unit_test::unit_test_log.set_stream(std::cout); } private: std::ofstream test_log; };
Ein Objekt dieser Klasse wird mit
in der Testdatei als globales Fixture erzeugt. Die Spalte zwei der Tabelle 20.1 enthält die möglichen, per Programm setzbaren Log-Level. Ein Log-Level schließt alle Ausgaben der Log-Level der darunterliegenden Tabellenzeilen ein.
Kommandozeile bzw. |
per Programm festlegbar |
protokolliert wird |
success |
log_successful_tests |
alles |
all |
alles |
|
test_suite |
log_test_units |
Suites und Testfälle |
message |
log_messages |
BOOST_TEST_MESSAGE-Nachricht |
warning |
log_warnings |
Warnungen |
error |
log_all_errors |
Fehler |
cpp_exception |
log_cpp_exception_errors |
nicht gefangene Exceptions |
system_error |
log_system_errors |
Systemfehler |
fatal_error |
log_fatal_errors |
kritische oder fatale Systemfehler |
nothing |
log_nothing |
nichts |
Wenn eine Log-Datei mit dem Fixture angelegt wird, werden Log-Level-Einstellungen per Kommandozeilen-Option ignoriert. Die per Programm einstellbaren Log-Level befinden sich im Namespace boost::unit_test.
20.2.3 | Prüf-Makros |
Sie haben schon verschiedene Prüf-Makros kennengelernt, es gibt aber noch weitere für verschiedene Zwecke. Vielen der Makros ist gemeinsam, dass es sie für die drei Level WARN, CHECK und REQUIRE gibt, die oben auf Seite 676 beschrieben werden. In diesen Fällen wird im Folgenden einfach <level> als Platzhalter für eine der drei Varianten geschrieben, ohne weiter auf sie einzugehen.
Hinweis
Die Beispiele finden Sie der Reihe nach in der Datei cppbuch/k20/testbeispiele.cpp. Einige Tests beziehen sich auf die Klasse Datum aus Abschnitt 8.3, einschließlich der Lösung der Übungsaufgaben. Viele der Tests schlagen fehl, damit man die Fehlermeldung im Log-File sehen kann.
Prüft die Bedingung (Beispiel siehe Listing 20.1, Seite 676).
Liefert bei ungültiger Bedingung den übergebenen Text.
Verhalten sich wie BOOST_CHECK_MESSAGE(false, Text) und BOOST_REQUIRE_MESSAGE(false, Text).
Prüft das Prädikat. Im Unterschied zu BOOST_<level>() wird eine Funktion oder ein Funktor, gegebenenfalls mit Parametern, übergeben. Jeder Parameter muss von runden Klammern umschlossen sein. Beispiel für die Funktion istSchaltjahr(int):
BOOST_AUTO_TEST_CASE( schaltjahrtest ) { BOOST_CHECK_PREDICATE( istSchaltjahr, (2024) ); }
Ist eine Dokumentationshilfe. Das Makro gibt den übergebenen Text aus. Ein Beispiel sehen Sie unten bei BOOST_<level>_THROW.
Definiert die Anzahl der zu erwartenden, das heißt absichtlich hervorgerufenen Fehler für den Testfall. Die ausgegebene Fehleranzahl eines Testfalls wird um diese Zahl reduziert. Ein Beispiel sehen Sie unten bei BOOST_<level>_THROW.
Prüft, ob der Ausdruck eine Exception des Typs Exceptiontyp oder einer davon abgeleiteten Klasse wirft. Falls keine Exception geworfen wird, gibt es einen Fehler. Beispiel:
// in der letzten Zeile provozierten Fehler nicht mitzählen: BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES( konstruktor_ungueltiges_Datum, 1 ) BOOST_AUTO_TEST_CASE( konstruktor_ungueltiges_Datum ) { BOOST_CHECK_THROW( Datum(30, 2, 2024), UngueltigesDatumException ); BOOST_TEST_MESSAGE( "Gegenprobe:˽" ); // soll fehlschlagen, kein Fehler BOOST_CHECK_THROW( Datum(1, 2, 2024), UngueltigesDatumException ); }
Wirkt wie BOOST_<level>_THROW, mit dem Unterschied, dass die geworfene Exception der Prüffunktion übergeben wird. Gibt diese Funktion false zurück, ist der Test fehlgeschlagen; gibt sie true zurück, ist er bestanden. Die Prüffunktion kann zum Beispiel prüfen, ob der Text der Exception korrekt ist oder ob sie überhaupt vom richtigen Typ ist. Der Test
wird bestanden. Bei einem gültigen Datum würde er fehlschlagen, weil keine Exception geworfen würde.
Schlägt fehl, wenn der Ausdruck eine Exception wirft. Ein Beispiel:
Es ist möglich, mehrere Anweisungen anstelle des Ausdrucks auszuführen, wenn sie in einen do-while (false)-Block gepackt werden:
BOOST_CHECK_NO_THROW( do { int tag = 30; Datum(tag, 2, 2024); } while (false));
Dieses Makro ist sehr hilfreich zum Aufspüren von Fehlern. Die Nachricht kann so gewählt werden, dass ein direkt zu einem Fehler führender Wert ausgegeben wird. In allen anderen Fällen tut das Makro nichts. Ein Beispiel:
BOOST_AUTO_TEST_CASE( konstruktor_ungueltiges_Datum_checkpoint ) { for (int tag = 1; tag < 30; ++tag) { BOOST_TEST_CHECKPOINT( "Datum˽mit˽Tag˽=˽" << tag ); Datum d(tag, 2, 2025); // Exception bei tag == 29: Fehler } }
Am Ende der Schleife, wenn tag den Wert 29 annimmt, gibt es eine Exception, weil das zu erzeugende Datum ungültig ist. Das Testprogramm erzeugt folgende Ausgabe:
Die letzte Zeile weist auf den fehlerhaften Wert hin. Ohne das Makro BOOST_TEST_CHECK-POINT würde diese Zeile fehlen. In diesem Beispiel ist der Fehler leicht zu sehen. In anderen Fällen, zum Beispiel wenn sehr viele Daten in einer Schleife eingelesen und verarbeitet werden, ist es hilfreich, wenn ein fehlererzeugender Datensatz dokumentiert wird.
Ein Beispiel für BOOST_REQUIRE(Bedingung) zeigt der folgende Test. Ein Scheitern wird als fatal angesehen.
Diese Makros vergleichen zwei Werte links und rechts mit relationalen Operatoren. Im Unterschied zu BOOST_<level> werden im Fall eines false-Ergebnisses die verglichenen Werte ausgegeben. Tabelle 20.2 zeigt die relationalen Makros.
Makro |
Wirkung wie |
BOOST_<level>_EQUAL(links, rechts) |
BOOST_<level>(links == rechts) |
BOOST_<level>_NE(links, rechts) |
BOOST_<level>(links != rechts) |
BOOST_<level>_GE(links, rechts) |
BOOST_<level>(links >= rechts) |
BOOST_<level>_GT(links, rechts) |
BOOST_<level>(links > rechts) |
BOOST_<level>_LE(links, rechts) |
BOOST_<level>(links <= rechts) |
BOOST_<level>_LT(links, rechts) |
BOOST_<level>(links < rechts) |
Die Makros BOOST_<level>_EQUAL und BOOST_<level>_NE sollten nicht für float- und double-Werte genommen werden, weil bitweise verglichen wird. Weitere Informationen zum Vergleich von float- und double-Werten finden Sie auf Seite 690. Dazu passende Makros finden Sie unten.
Dieses Makro prüft, ob die ersten zwei Werte relativ (nicht absolut) dicht beieinanderliegen. Der dritte Wert gibt die Toleranz in Prozent an.
Nach boost/test/tools/floating_point_comparison.hpp ist der Test dann erfolgreich, wenn
gilt, wobei D gleich fabs(links - rechts) ist, also dem Absolutbetrag der Differenz. links und rechts müssen vom selben Typ sein. Das in Abschnitt 21.2.1 angesprochene Overflow-/Underflow-Problem wird durch Einsetzen des Maximalwerts für den Datentyp bzw. 0 gelöst.
Arbeitet wie BOOST_<level>_CLOSE, nur dass die Toleranz anders definiert wird. Der Test ist erfolgreich, wenn fabs(toleranz) > (fabs(rechts/links) - 1)) ist.
Ist erfolgreich, wenn der Betrag des Werts kleiner als der Betrag der Toleranz ist.
Vergleicht zwei Container (Collections) auf gleiche Inhalte, ähnlich wie der Algorithmus mismatch von Seite 786. Im Unterschied zu Letzterem muss das Ende des zweiten Bereichs angegeben werden. Unterschiedliche Elemente werden gemeldet. Dabei können Container der Standardbibliothek und einfache C-Arrays verwendet werden, sogar gemischt, wie das Beispiel zeigt:
BOOST_AUTO_TEST_CASE( eq_collection ) { std::vector<int> v; for (int i = 1; i <= 8; ++i) { v.push_back(i); } int arr [] = { 1, 2, 3, 5, 5, 6, 7, 9}; // Test schlägt fehl: BOOST_CHECK_EQUAL_COLLECTIONS(v.begin(), v.end(), arr, arr+8 ); }
Prüft, ob alle Bits der zwei Werte übereinstimmen. Falls nicht, schlägt der Test fehl und es werden alle nicht übereinstimmenden Positionen ausgegeben.
20.2.4 | Kommandozeilen-Optionen |
Die Steuerung des Log-Levels mit einer Kommandozeilen-Option wird oben auf Seite 677 beschrieben. Daneben gibt es weitere Optionen, von denen die wichtigsten in Tabelle 20.3 genannt werden. Alle Optionen sind auch durch entsprechende Umgebungsvariablen einstellbar. Die Spalte zwei zeigt die möglichen Werte. Der voreingestellte Wert ist jeweils zuerst genannt.
Option |
Werte |
Bedeutung |
auto_start_dbg |
no, yes |
Bei Systemfehler Debugger starten |
build_info |
no, yes |
Anzeige der Compiler-Version |
catch_system_errors |
yes, no |
no: Systemfehler werden nicht aufgefangen. Sie können damit von einem übergeordneten Programm (GUI) analysiert werden. |
detect_memory_leak |
1, 0, > 1 |
nur für MS-Compiler (Details siehe [Roz]) |
detect_fp_exceptions |
no, yes |
Floating Point-Exceptions fangen (falls vom System unterstützt) |
log_format |
HRF |
HRF = human readable format (ASCII-Text) |
XML |
für weitere automatisierte Verarbeitung |
|
report_format |
dasselbe wie log_format |
|
output_format |
dasselbe wie log_format, hat aber ggf. Vorrang vor den beiden anderen |
|
random |
0 |
Tests der Reihe nach ausführen |
1 |
zufällige Reihenfolge, basierend auf der aktuellen Zeit |
|
z >1 |
zufällige Reihenfolge mit z als Initialisierungswert (seed) |
|
result_code |
yes, no |
no: es wird stets 0 zurückgegeben (bei Einbindung in GUI von Interesse) |
run_test |
durchzuführende Testfälle und -suiten. Wildcards sind erlaubt. Beispiele: |
|
testprog -runtest=test_a |
||
testprog -runtest=test_suite,test_c,xtest* |
||
show_progress |
no, yes |
Anzeige eines Fortschrittsbalkens |
log_level |
siehe Seite 677 |
Die entsprechende Umgebungsvariable ergibt sich aus der Option, indem die Option in Großschreibung an die Zeichenkette BOOST_TEST_ gehängt wird. So gehört zu der Option show_progress die Umgebungsvariable BOOST_TEST_SHOW_PROGRESS. Die einzige Ausnahme ist nach [Roz] die Umgebungsvariable BOOST_TESTS_TO_RUN zur Option run_test.
20.3 | Test Driven Development |
Mit dem Aufkommen der agilen Softwareentwicklung (http://agilemanifesto.org/) nimmt die Bedeutung der testgetriebenen Entwicklung (englisch TDD = Test Driven Development) zu. Dabei wird zuerst ein Testfall spezifiziert, also bevor der zu prüfende Code überhaupt existiert. Der Testfall wird mit einem XUnit-Werkzeug ausgeführt und schlägt natürlich fehl. Anschließend entsteht nach und nach der Programmcode, wobei der Test wiederholt ausgeführt wird – so lange, bis er bestanden wird. Im folgenden Schritt wird der nächste Testfall spezifiziert und der Ablauf wiederholt. Dabei werden auch alle vorherigen Testfälle ausgeführt, um sicher zu sein, dass eine Änderung nicht andere Programmteile ungünstig beeinflusst. Dieses Vorgehen hat einige Vorteile:
Die Planung der Testfälle setzt eine gründliche Auseinandersetzung mit der Anforderungsdefinition und dem Design voraus. Dabei entstehende Unklarheiten und Widersprüche werden noch vor der Codierung beseitigt.
Die Testfälle dienen als Spezifikation für die zu erstellende Software. Damit reduziert sich der nachträgliche Testaufwand.
Es wird keine Software geschrieben, die nicht gebraucht wird. Ich habe gelegentlich beobachtet, dass beim Schreiben einer Klasse in guter Absicht möglicherweise nützliche Methoden gleich mitprogrammiert werden, ohne dass klar ist, ob sie jemals gebraucht werden. In der industriellen Wirklichkeit führt dies zu unnötigen Kosten – jede Methode muss zusätzlich getestet und dokumentiert werden.
Die regelmäßigen Regressionstests garantieren bei Erfolg, dass die Software ein (durch die Testfälle definiertes) Mindestmaß an Qualität besitzt.
Der in manchen Projekten am Ende zu beobachtende verstärkte Zeitdruck führt zu Aussagen wie »Zum ausführlichen Testen haben wir keine Zeit mehr!«, verbunden mit teuren Änderungen nach Auslieferung der Software. Die Wahrscheinlichkeit für solche Probleme ist bei der testgetriebenen Entwicklung wegen der ständig mitlaufenden Tests gering.
Ein testgetrieben entwickeltes System ist normalerweise von höherer Qualität als ein auf herkömmliche Art entwickeltes mit einem Test nach Abschluss der Programmierung. Daraus folgt jedoch nicht unbedingt, dass es ausreichend getestet ist, nämlich dann, wenn die Testfälle nicht systematisch auf Basis der Anforderungsdefinition und unter Verwendung guter Testverfahren entwickelt wurden. Das Qualitätsproblem verlagert sich von der Programmierung auf die Testfallspezifikation. Ein weiterer negativer Aspekt ist die isolierte Betrachtung des aktuellen Testfalls. Bei sofortiger Kenntnis aller Anforderungen an eine Komponente ist möglicherweise ein besseres Design möglich und es müssten in einer Klasse nicht nachträglich viele Änderungen vorgenommen werden. Diese Argumente sprechen nicht gegen die testgetriebene Entwicklung, wenn man sie berücksichtigt.