20
Unit-Test

Dieses Kapitel behandelt die folgenden Themen:

Image       Unit-Test

Image       Werkzeuge

Image       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:

Image       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.

Image       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].

Compilations- und Link-Optionen

Es wird davon ausgegangen, dass die Boost-Libraries installiert sind. Die Optionen sind:

Image       Statisches Linken der Boost-Test-Library

Image       Dynamisches Linken der Boost-Test-Library

Image       »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.

Image       »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.

Testaufbau

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.

Weil main() automatisch erzeugt wird, ist die Test-Suite nach Compilation lauffähig. Die Ausgabe des Programms ist

Running 2 test cases... *** No errors detected

Falls nun fälschlicherweise 4 statt 3 im ersten Test eingetragen wird, ist die Ausgabe

Running 2 test cases... einfach.cpp(9): error: in "einfacher_stringtest/laenge": check s.length() == 4 has failed *** 1 failure is detected in the test module "Master Test Suite"

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:

Image       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.

Image       BOOST_CHECK(Bedingung): Die Nichterfüllung der Bedingung wird als Fehler gesehen und gemeldet. Das Testprogramm läuft weiter.

Image       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:

Image       Vorbereitende Maßnahmen (setUp)

Image       Test durchführen

Image       Ergebnis prüfen und protokollieren

Image       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:

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:

testprog.exe --log_level=error

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:

Ein Objekt dieser Klasse wird mit

BOOST_GLOBAL_FIXTURE( LogKonfiguration );

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.

Tabelle 20.1: Log-Level

Kommandozeile bzw.
BOOST_TEST_LOG_LEVEL

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.

Image

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.

Image

BOOST_<level>(Bedingung)

Prüft die Bedingung (Beispiel siehe Listing 20.1, Seite 676).

BOOST_<level>_MESSAGE(Bedingung, Text)

Liefert bei ungültiger Bedingung den übergebenen Text.

BOOST_ERROR(Text) und BOOST_FAIL(Text)

Verhalten sich wie BOOST_CHECK_MESSAGE(false, Text) und BOOST_REQUIRE_MESSAGE(false, Text).

BOOST_<level>_PREDICATE(Prädikat, Argumente)

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_TEST_MESSAGE(Text)

Ist eine Dokumentationshilfe. Das Makro gibt den übergebenen Text aus. Ein Beispiel sehen Sie unten bei BOOST_<level>_THROW.

BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(testfall, Anzahl)

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.

BOOST_<level>_THROW (Ausdruck, Exceptiontyp)

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:

BOOST_<level>_EXCEPTION (Ausdruck, Exceptiontyp, Prüffunktion)

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

BOOST_CHECK_EXCEPTION( Datum(30, 2, 2024), UngueltigesDatumException, checkException);

wird bestanden. Bei einem gültigen Datum würde er fehlschlagen, weil keine Exception geworfen würde.

BOOST_<level>_NO_THROW(Ausdruck)

Schlägt fehl, wenn der Ausdruck eine Exception wirft. Ein Beispiel:

BOOST_CHECK_NO_THROW( Datum(30, 2, 2024));

Es ist möglich, mehrere Anweisungen anstelle des Ausdrucks auszuführen, wenn sie in einen do-while (false)-Block gepackt werden:

BOOST_TEST_CHECKPOINT(Nachricht)

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:

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:

unknown location(0): fatal error: in "test_suite_datum_und_anderes/konstruktor_ungueltiges_Datum_checkpoint": UngueltigesDatumException: Ungueltiges Datum! testbeispiele.cpp(83): last checkpoint: Datum mit Tag = 29

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.

Datumdifferenztest mit REQUIRE

Ein Beispiel für BOOST_REQUIRE(Bedingung) zeigt der folgende Test. Ein Scheitern wird als fatal angesehen.

BOOST_AUTO_TEST_CASE(differenz) { Datum d(1, 1, 1800); Datum vortag; Datum ende(31, 12, 2300); while (d != ende) { vortag = d; ++d; BOOST_REQUIRE(vortag < d); // operator<() testen BOOST_REQUIRE(datumDifferenz(vortag, d) == 1); } }

Relationale Makros

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.

Tabelle 20.2: Relationale 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)

BOOST_AUTO_TEST_CASE(relational) { BOOST_CHECK_EQUAL(17, 17); } // Test auf Gleichheit

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.

BOOST_<level>_CLOSE(links, rechts, toleranz)

Dieses Makro prüft, ob die ersten zwei Werte relativ (nicht absolut) dicht beieinanderliegen. Der dritte Wert gibt die Toleranz in Prozent an.

BOOST_CHECK_CLOSE( 1.25, 1.27, 1.0); // schlägt fehl BOOST_CHECK_CLOSE( 1.25, 1.27, 2.0); // ok, Differenz ist kleiner als 2 %

Nach boost/test/tools/floating_point_comparison.hpp ist der Test dann erfolgreich, wenn

(D/fabs(links) <= fabs(toleranz)) && (D/fabs(rechts) <= fabs(toleranz))

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.

BOOST_<level>_CLOSE_FRACTION(links, rechts, toleranz)

Arbeitet wie BOOST_<level>_CLOSE, nur dass die Toleranz anders definiert wird. Der Test ist erfolgreich, wenn fabs(toleranz) > (fabs(rechts/links) - 1)) ist.

double toleranz = 0.016001; BOOST_CHECK_CLOSE_FRACTION(1.25, 1.27, toleranz);

BOOST_<level>_SMALL(Wert, Toleranz)

Ist erfolgreich, wenn der Betrag des Werts kleiner als der Betrag der Toleranz ist.

BOOST_<level>_EQUAL_COLLECTIONS

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_<level>_BITWISE_EQUAL(a, b)

Prüft, ob alle Bits der zwei Werte übereinstimmen. Falls nicht, schlägt der Test fehl und es werden alle nicht übereinstimmenden Positionen ausgegeben.

BOOST_AUTO_TEST_CASE(eq_bit) { BOOST_CHECK_BITWISE_EQUAL(7, 9); }
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.

Tabelle 20.3: Kommandozeilen-Optionen

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:

Image       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.

Image       Die Testfälle dienen als Spezifikation für die zu erstellende Software. Damit reduziert sich der nachträgliche Testaufwand.

Image       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.

Image       Die regelmäßigen Regressionstests garantieren bei Erfolg, dass die Software ein (durch die Testfälle definiertes) Mindestmaß an Qualität besitzt.

Image       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.