Suchen

Qualitätssicherung Erfahrungen aus dem Einsatz des Testwerkzeugs CppUnit

| Autor / Redakteur: Richard Kölbl * / Franz Graser

Das freie Unit-Testwerkzeug CppUnit eignet sich dazu, Programm-Module mit meist geringem Aufwand zu isolieren und auf ihre Funktionalität zu testen. Hier ein Erfahrungsbericht.

Firmen zum Thema

Software unter der Lupe: Mit Unit-Tests können einzelne Programm-Module mit meist geringem Aufwand isoliert und auf ihre vertragsgemäße Funktionalität getestet werden.
Software unter der Lupe: Mit Unit-Tests können einzelne Programm-Module mit meist geringem Aufwand isoliert und auf ihre vertragsgemäße Funktionalität getestet werden.
(Bild: Clipdealer)

Ohne Tests sind keine Aussagen über die Qualität eines Codes möglich. Diese Tatsache dürfte unmittelbar einsichtig sein. Wozu aber Unit-Tests, deren Erstellung je nach Testtiefe einiges an Aufwand bedeutet, der gerade in kleineren Entwicklertrupps zumindest zeitweise zu einer Erhöhung des Arbeitsanfalls führt?

1. Einsatzbereich und Grenzen von Unit-Tests

Unit-Tests machen sich den modularen Aufbau eines Softwareprogrammes dahingehend zunutze, dass einzelne Programmteile, nachfolgend als Module bezeichnet, mit meist geringem Aufwand isoliert und auf ihre vertragsgemäße Funktionalität getestet werden können. Es sind klassische Black-Box-Tests, die nicht auf das Wie der Lösung abzielen, sondern nur auf das Ob: Erfüllt ein spezifisches Modul im vollen Umfang - aber nicht darüber hinaus - die vertragsgemäße Funktionalität oder nicht.

Ich sehe in dieser Darstellung von dem Fall ab, dass zuerst Tests für Module entwickelt werden, und dann erst der Code selbst (Test-driven-development). Ich habe zum einen keine Erfahrung damit, zum anderen weiß ich nicht, ob die oft genannten Vorteile (z.B. klarere APIs) dieses Vorgehen in der Tat so günstig machen, wie es von seinen Anhängern dargestellt wird. Ich habe bislang auch noch keine ausreichend kritische Auseinandersetzung mit den sicher ebenfalls existenten Nachteilen dieses Verfahrens gesehen. Der Ausgangspunkt ist hier also bereits existierender Code, der über eine hinreichende funktionelle Definition und Abgrenzung zu anderen Codestücken verfügt, sei es auch lediglich in Form von Kommentaren.

Wenn keine Unit-Tests gemacht werden, hat man zwei entscheidende Nachteile:

1. Es gibt keine Grundlage für eine Aussage, ob und in welchem Umfang der Code korrekt funktioniert.

2. Vor allem bei Erweiterungen und Bugfixing kann nie ausgeschlossen werden, dass die Codestruktur (Daten- und Kontrollfluss) nicht so verändert wurde, dass die festgelegte Funktionalität nicht mehr erbracht wird. Es ist nicht selten so, dass solche Veränderungen entweder schwer zu detektieren sind oder erst in bestimmten Konstellationen auftreten. Module reichen ihre jeweiligen Ergebnisse meist an andere weiter. Deshalb zeigen sich die Symptome der fehlerhaften Codestelle gerne an ganz anderer Stelle. Wenn Software erst an eine genügend hohe Anzahl von Kunden weitergegeben wurde, kann dieser Fall unter Umständen sehr schnell auftreten. In einem Gesamtgerätetest von einem Oberflächensymptom auf einen logischen Fehler in einem tiefer liegenden Modul zu schließen, ist oft eine weitaus zeitraubendere Aufgabe, als von vorneherein Tests für Module zu schreiben. Zumindest für die Module, deren Fehlfunktion ernsthafte Auswirkungen auf den Funktionsablauf des Gerätes haben.

Die Aufgabe des Unit-Testcodes besteht somit darin, die vollständige und vertragsgemäße Funktionalität inklusive des Verhaltens an den Grenzen zu ungültigen Parametern abzutesten sowie über den Vertrag hinausgehende Funktionsumfänge auszuschließen (!). Auch die Robustheit des Moduls sollte mit berücksichtigt werden: falsche Eingaben sollten es nicht zum Absturz bringen.

Der Pflegeaufwand für einen Unit-Test nach der Erstellung des eigentlichen Testgerüsts ist verhältnismäßig gering, solange sich am Konzept des Moduls nichts Entscheidendes ändert; meist müssen Tabellen für die Parametrisierung geändert werden, unter Umständen auch Anpassungen an den Schnittstellen vorgenommen werden. Dies bringt den enormen Vorteil der Reproduzierbarkeit der Tests mit sich. Wenn also nach einer gewissen Zeit der Code erweitert wird - Umzug auf eine neue Plattform, neue, vom Kunden geforderte Funktionalitäten eingebaut und dergleichen - werden zwangsläufig bestimmte Module umgebaut werden müssen, die wieder an andere, unverändert gebliebene ihre Zwischenwerte weiterreichen. Mit einer dann bereits bestehenden und bewährten Testumgebung kann viel schneller eine belastbare Aussage über die Codequalität getroffen werden. Wenn keine Unit-Tests vorliegen, ist schlicht überhaupt keine Qualitätsaussage möglich.

Nicht nur einzelne Module sind testbar, sondern ein Unit-Test kann im Prinzip so organisiert werden, dass die Zusammenarbeit mehrerer oder aller Module getestet wird. Dies ist aber ein fortgeschrittener Unit-Test, der sich mit zunehmender Anzahl der Module in Richtung Integrationstest bewegt. Hier wandert die Sicht vom einzelnen Modul als Black Box mit zunehmendem Integrationsgrad auf eine Gruppe von Modulen, die zusammen eine größere funktionelle Einheit darstellen. Der Blick auf diese Gruppe ist wieder der auf eine Black Box, da das gesamte Zusammenspiel betrachtet wird und die (aus früheren Testphasen natürlich bekannten) Interna innerhalb der Modulgruppe ausgeblendet werden.

Die wesentliche Grenze des Unit-Tests liegt darin begründet, dass der Programmcode in der Regel losgelöst von der Hardware ausgeführt wird. Der Schwerpunkt des Unit-Tests muss damit zwangsläufig auf der logischen Struktur liegen. Hardwarezugriffe, Problematiken von Interrupts, Timingprobleme und weitere, erst zur Laufzeit sich ergebende problematische Konstellationen des Gesamtgeräts können mit dem Unit-Test nur bedingt abgetestet werden. Immerhin kann auch für solche Fälle mindestens die Robustheit des Codes überprüft werden: dass beispielsweise bei fehlender Antwort der Hardware nicht das gesamte System hängt oder abstürzt (NULL-Pointer!).

Es darf an dieser Stelle daran erinnert werden, daß ein Test dann als erfolgreich anzusehen ist, wenn er einen Fehler gefunden hat. Es gibt empirische Gründe wie auch handfeste mathematische Beweise dafür, dass Software mit zunehmendem Komplexitätsgrad eine zunehmende Anzahl potentieller Restfehler enthält (z.B. SPILLNER & LINZ 2005). So viele Fehler davon wie möglich zu finden und die Qualität des Codes zu bestimmen sind die zwei zentralen Aufgaben des Testens. Selbstverständlich muß die Testphase irgendwann auch beendet werden; bei entsprechend sinnvollem und ergebnishöffigem Vorgehen dabei kann die Qualitätsaussage über den Code mit höherer Wahrscheinlichkeit getroffen werden und - mit Vorsicht - können die verbleibenden Restfehler als potentiell weniger schadensträchtig eingestuft werden: Risiko errechnet sich aus der Eintrittswahrscheinlichkeit eines schadensträchtigen Ereignisses innerhalb eines bestimmten Zeitraumes multipliziert mit dem größtmöglichen Schaden. Wenn die Eintrittswahrscheinlichkeit durch das Testen gesenkt werden kann, reduziert sich das Riskio dementsprechend. Andererseits ergibt sich nach dem Rollout und die vielfache Verbreitung der Software eine erheblich größere Auftrittswahrscheinlichkeit für Fehler, was in die Risikobetrachtung Eingang finden sollte.

2. Testbarkeit von Modulen

Der Aufwand der Erstellung eines Unit-Tests bemisst sich deutlich nach dem Grad der Testbarkeit. Nicht jedes Stück Software ist von vorneherein testbar, sondern eine gewisse grundlegende Testbarkeit muss mit berücksichtigt und von vorneherein eingebaut werden. Idealerweise besitzt ein Modul eine definierte Schnittstelle für Ein- und Ausgabewerte. Das können durchaus Zugriffsfunktionen sein (Set/GetVariable), die nur in der Debugversion vorhanden sind oder friend-class-Deklarationen u. dgl. Über die Legitimität solcher rein für Testzwecke mit eingeplanter Strukturen gibt es einen bemerkenswerten Artikel [RODEN 2010], der sie sinngemäß so begründet: jede Maschine habe ihre Wartungsklappe, die sonst auch keinem weiteren produktiven Zweck dient. Testschnittstellen könnte man also als eine Art Motorhaube bezeichnen, um nachsehen zu können, wo es leckt; während der Fahrt ist sie natürlich geschlossen. In der Praxis stößt dies nicht selten bei Sicherheitsvorgaben an Grenzen, die bestimmte Codeteile oder Variablenbelegungen aus gutem Grund nicht nach außen sichtbar machen lassen. Hier muss im Einzelfall entschieden werden, ob eine Debugversion mit nur für den Test sichtbaren Schnittstellen machbar ist oder ob im Extremfall bestimmte Werte schlichtweg nicht beobachtet werden können. Letzteres zählt zu den zahlreichen Grenzen des Testens. Grundsätzlich soll in den zu testenden Code nach Möglichkeit aber nicht eingegriffen werden.

Abbildung 1: Kriterien für die Testbarkeit von Modulen im Überblick
Abbildung 1: Kriterien für die Testbarkeit von Modulen im Überblick
(Grafik: Mixed Mode)

Die Testbarkeit von Modulen beruht auf folgenden Minimalvoraussetzungen, wie zu Schlagworten zusammengefasst in Abbildung 1 dargestellt:

1. Die Kontur des zu testenden Moduls soll einer definierten Funktionalität entsprechen. Das bedeutet, dass funktionell zusammengehörige Klassen, Headerfiles oder sonstige architektonische Elemente als ein Modul definiert werden können. Dabei soll keine Verschmierung der Funktionalität auf mehrere Module stattfinden. Auf diesen Aspekt sollte bereits bei der Erstellung der Softwarearchitektur Augenmerk gelegt werden. Gewachsener Code zeigt typischerweise starke Tendenzen zur Überlappung von Funktionalitäten und schlechte Abtrennbarkeit einzelner Module.

2. Die Funktionalität sollte möglichst vollständig definiert sein. Es sollten keine Kombinationen von Eingabeparametern (Übergabewerte an Funktionen/Methoden, Zustände von State Machines u .dgl.) existieren, deren Resultat bei der ordnungsgemäßen Verarbeitung durch das Modul nicht vorher definiert wurde.

3. Der Testverlauf soll durch geeignete Parametereingaben im Sinne einer aussagekräftigen Verifizierung beeinflussbar sein.

4. Die vertragsgemäße Verarbeitung der Eingabeparameter soll an einem oder mehreren Ergebnisparametern überprüft werden können.

3. Definition eines Moduls

Die Definition eines funktionellen Moduls ist in der Praxis nicht immer so einfach; vor allem, wenn es nicht objektorientierter Code ist, der aus einer Vielzahl globaler Funktionen besteht, die einander zuarbeiten. Hier kann es durchaus sinnvoll sein, sehr viele oder gar alle globale Funktionen zu einem globalen Modul zusammenzufassen und für jede Teilfunktion eine eigene Testfunktion vorzusehen. Es gibt hierbei keine einzig richtige Lösung, sondern jeweils nur einen mehr oder weniger pragmatisch-praktikablen Ansatz, der sich aus der Logik des zu testenden Codes ergibt. Auch kann sich die Abgrenzung des zu testenden Moduls während der Testimplementierung ändern; Funktionen können wegfallen oder andere hinzutreten. Die Implementierung des Codes für einen Test ist die schärfste Analysephase und legt nicht selten Modulgrenzen, aber auch Mängel bei dessen Definition und Implementierung klarer frei, als es das beste Review tun könnte. Auch dies ist einer der unschätzbaren Vorteile des Unit-Tests: der Code muss sich im harten Einsatz erstmals bewähren. Schließlich: die Implementierung eines Tests auf ein bestimmtes Modul ist ein impliziter Test auf die Qualitätskriterien Wartbarkeit und Verständlichkeit des Codes, um so mehr, als im Idealfall jemand anderes als der ursprüngliche Entwickler den Unit-Test entwirft und implementiert.

Einfacher liegt der Fall einer Klasse mit Membervariablen und -funktionen. Hier kann man in der Regel davon ausgehen, dass die Klasse gleichzeitig das funktionelle Modul darstellt.

4. Begriffsdefinitionen in der CppUnit-Welt: Testmodule und Testsuiten

CppUnit-Tests arbeiten mit C++-Klassen, Makros etc., die das zu erzeugende Testprogramm (eine gewöhnliche exe-Datei) strukturieren. Diese Struktur weicht von der in der Testliteratur genannten etwas ab, deshalb ist hier eine Erläuterung nötig.

Beiden obengenannten Modulvarianten - einmal dem Bündel von zu einem Modul zusammengefassten klassenlosen Funktionen, im anderen der Zuordnung 1 Klasse = 1 Modul - steht auf der Seite des Unit-Tests dem Modul jeweils eine Testklasse gegenüber. Eine Testklasse befasst sich also mit jeweils einer definierten Funktionalität.

Ein Modul enthält meist mehrere Funktionen und Methoden, denen jeweils in der Testklasse des CppUnit eine Testmethode gegenübersteht. Diese Testmethode ruft eine Modulfunktion bzw. -methode auf und übergibt die für den jeweiligen Testfall definierten Parameter. Die Terminologie des Testens wird leider nicht überall einheitlich verwendet (z.B. SPILLNER & LINZ 2005). Deshalb definiere ich in Anlehnung an die von dem CppUnit-Paket vorgegebene Struktur einen einzelnen Testfall im Rahmen dieses Beitrags wie folgt:

Ein Testfall eines CppUnit-Testprojekts ist der Aufruf einer zu testenden Funktion unabhängig von ihrer Parametrisierung. Eine bestimmte Funktion des zu testenden Codes hat eine oder mehrere Eingabevariablen, und der Aufruf dieser Funktion ist ein einzelner Testfall. Dies begründet sich aus der internen Weiterverarbeitung im Programmcode des CppUnit, bei der eine Testmethode mit der Instanz eines Objektes Test verknüpft wird.

Abbildung 2: Rechner mit den vier Grundrechenarten. Dargestellt ist das Modul Division mit der Funktion „Dividiere(x, y)“. x und y stellen die Eingabewerte des Moduls dar, mit x/y ist die Modulfunktionalität dargestellt und f(x,y) ist der Ausgabewert.
Abbildung 2: Rechner mit den vier Grundrechenarten. Dargestellt ist das Modul Division mit der Funktion „Dividiere(x, y)“. x und y stellen die Eingabewerte des Moduls dar, mit x/y ist die Modulfunktionalität dargestellt und f(x,y) ist der Ausgabewert.
(Grafik: Mixed Mode)

Ein Testfall ist im Idealfall eine einzelne, atomare Anfrage an das zu testende Stück Code, wie es auf die spezifischen Eingabebedingungen reagiert. Die zu erwartende Reaktion (Soll-Reaktion) muss vorher festliegen, damit das tatsächliche Ergebnis (Ist-Wert) damit verglichen und die Entscheidung über Test bestanden (PASS) oder nicht bestanden (FAIL) getroffen werden kann. Nur in begründeten Einzelfällen darf von diesem Grundprinzip abgewichen werden. Wie ein einzelner Testfall am besten abgefasst sein sollte, damit er eine atomare Anfrage stellt, kann hier nur knapp angerissen werden, da diese Frage auf einen gleichartig komplexen Hintergrund abzielt, wie bei der Abfassung von Methoden und Funktionen generell zu verfahren ist. Zentrales Leitmotiv ist "atomar", das heißt: ein Testfall sollte eine Funktionalität möglichst so weit reduzieren und in Einzelschritte zerlegen, wie einesteils es sinnvoll und notwendig ist, und andersteils die Fehlerursache ohne allzu großen Aufwand gefunden werden kann. Im Unit-Test auf möglichst tiefer Ebene der Funktionen einzusteigen ist daher sinnvoll. Ruft beispielsweise eine Funktion mehrere Unterfunktionen auf, die jeweils eine elementare Aufgabe erledigen, wird es oft vorteilhaft sein, diese Unterfunktionen einzeln abzutesten. Als Beispiel sei ein Rechner für die vier Grundrechenarten gewählt. Er enthalte für jede Funktionalität (sprich Grundrechenart) ein Modul, das nur diese eine Grundrechenart implementiert. Die Module (ein Beispiel in Abb. 2) erfüllen die Anforderungen an Testbarkeit.

Dem Modul Division steht das Testmodul CDivisionTest gegenüber. Es ist eine eigene Klasse, die die Methode (= Testfall im Sinne von CppUnit) TestDivision (x,y) enthält (Abb. 3). Mit der Parametrisierung x = 5 und y = 0 wird an das Modul Division durch TestDivision(5,0) die Frage gestellt, wie es sich bei einer Division durch 0 verhält, anders gesagt, ob der Programmierer die Division durch Null abgefangen hat. Andere Parametrisierungen derselben Testmethode, etwa TestDivision (25,5) oder TestDivision (10,3) stellen die Frage nach der korrekten Implementierung der zu testenden Modulfunktion im Falle einer sinnvollen Parametrisierung, zielt also auf ein anderes Testziel ab. Leider ist es bei CppUnit so, dass die einzelne Ausführung einer Testmethode mit einer ganz bestimmten Parametrisierung nicht als Einzeltest angesehen wird; hier geht CppUnit deutlich andere Wege als in der Literatur häufig beschrieben. Als einzeln ausführbaren Test bietet CppUnit nur die Testsuite an.

Eine Testsuite bei CppUnit ist die Menge aller Testmethodenaufrufe eines zu testenden Moduls mit unterschiedlichen Parametern, mithin die Menge aller Testfälle, die auf eine Funktionalität des Originalcodes abzielt. Im Beispiel des Grundrechners gibt es vier Testsuiten: TEST_ADDITION, TEST_SUBTRAKTION, TEST_MULTIPLIKATION, TEST_DIVISION. Die Testsuite TEST_DIVISION enthält in unserem Beispiel die Testmethode TestDivision(x,y), die wir mit den Parametrisierungen x=5 y=0, x=25 y=5 und x=10 y=3 ausgeführt haben. Eine Testsuite prüft in CppUnit also eine Funktionalität unter verschiedenen Aspekten ab (Abb. 3).

Abbildung 3: Zusammenfassende Darstellung der bisher behandelten Begriffe Modul, Testmodul, Testmethode und Testsuite. Nähere Erläuterung siehe Text.
Abbildung 3: Zusammenfassende Darstellung der bisher behandelten Begriffe Modul, Testmodul, Testmethode und Testsuite. Nähere Erläuterung siehe Text.
(Grafik: Mixed Mode)

Es wäre günstiger, die Ausführung einer Testmethode mit einer definierten Parametrisierung auch im CppUnit als einzelnen Test zu benennen, da die Auswahl der Parameter jeweils auf ganz unterschiedliche Testfragen abzielt: Funktionen haben oftmals Übergabeparameter oder sind von sonstigen, z.B. globalen Systemparametern abhängig. Diese Parameter haben notwendigerweise gültige (definierte) und nicht gültige Bereiche. Gültige, also im definierten Bereich liegende Parameter überprüfen die korrekte Funktionalität im Gutfall, während der Aufruf derselben Funktion mit ungültigen Parametern die Robustheit bei Falscheingaben abtestet. Ein Test sollte mindestens je einen Wert im definierten und nicht definierten Werteraum sowie die Grenzen abtesten; um so mehr, wenn es um Größenvergleiche geht (<, >, ≤ usw.) Datumswerte sollten an den nicht existenten Tagen (30. Februar!) sowie Uhrzeiten sinngemäß ebenfalls getestet werden.

Eingabewerte können bei CppUnit entweder festcodiert eingegeben werden oder, was besser ist, aber auch mehr Aufwand bedeutet, aus einer Parameterliste eingelesen werden. Das hat den Vorteil, dass ein Testlauf leichter variiert werden kann, indem nur die Parameterliste variiert wird, nicht aber in den Code des CppUnit-Tests eingegriffen wird. Ebenso können Zwischen- und Endergebnisse von Moduloperationen in Logfiles ausgegeben werden, CppUnit macht das nicht automatisch. Es ergibt sich die in Abbildung 4 dargestellte Architektur eines CppUnit-Tests mit Ein- und Ausgabefiles.

Abbildung 4: Bei CppUnit-Tests ist es vorteilhaft, Eingabeparameter nicht fest zu codieren, sondern in einer einfach zu ändernden Eingabeliste vorzuhalten. Ausgabeparameter schreibt CppUnit nicht automatisch in eine Logdatei. Der Benutzer muss sie selbst erstellen.
Abbildung 4: Bei CppUnit-Tests ist es vorteilhaft, Eingabeparameter nicht fest zu codieren, sondern in einer einfach zu ändernden Eingabeliste vorzuhalten. Ausgabeparameter schreibt CppUnit nicht automatisch in eine Logdatei. Der Benutzer muss sie selbst erstellen.
(Grafik: Mixed Mode)

5. Probleme bei der Erstellung von Testmethoden in CppUnit

In der Praxis treten verschiedene Probleme bei der Erstellung der Testfunktionen auf:

a) Mangelnde Zugreifbarkeit von Variablen und/oder Funktionen des zu testenden Moduls. Dieser Punkt rührt an die Testbarkeit des Codes. Hier gibt es verschiedene Möglichkeiten, dieses Problem zu umgehen. Allerdings kann es auch Fälle geben, in denen der zu testende Code so gut gekapselt ist, dass er auf diese Weise nicht testbar ist.Möglichkeiten sind:

a) Aufruf einer tieferliegenden, gekapselten Funktion über eine höhere, offenliegende. Dabei muss man die Ausführung anderer Funktionen in Kauf nehmen.

b) Verschiebung der Deklarationen aus einer *.cpp in eine Headerdatei.

c) Bei Klassen: Zugänglichmachung der zu testenden Klasse für die Testklasse durch die Deklaration "friend".

d) Implementierung von Set/Get-Funktionen u. dgl. in der zu testenden Klasse.

Es ist klar, dass die Schritte b) bis d) jeweils einen massiven Eingriff in den zu testenden Code darstellen, der strenggenommen Aussagen über die Codequalität nur in bezug auf den so umgestalteten Code zulässt. Von daher ist die bereits in der Designphase des Codes eingeplante "Wartungsklappe" für den Code eindeutig die bessere Alternative.

b) Fehlender Hardwarezugriff.

Durch die Loslösung von der Hardware müssen die von dort kommenden Werte anderen Quellen entnmmen werden. Dazu sind in der Regel Eingabelisten vorgesehen, in denen die scheinbar von der Hardware stammenden Parameter vorher festgelegt werden. Auf diese Weise kann der Testablauf gesteuert werden, indem beispielsweise gültige und ungültige von der Hardware kommende Werte vorgegeben werden. Auch dies ist ein entscheidender Vorteil: oft ist es kaum bzw. unmöglich - wenngleich in der Praxis nie völlig auszuschließen - von der wirklichen Hardware einen Fehlwert zu bekommen, um so die Robustheit des Systems auf derartige Situationen zu prüfen.

6. Das CppUnit-Paket

Das Open Source-Softwarepaket CppUnit (http://sourceforge.net/projects/cppunit/) bietet mehrere verschiedene Pakete in Form von Libraries, Dlls und Executables, mit deren Hilfe Unit-Tests automatisch durchgeführt werden können. CppUnit ist auf mehreren Plattformen (z. B. Linux, Windows XP etc.) sowie mit anderen Programmiersprachen einsetzbar. Zum Hintergrund des CppUnit-Pakets siehe:

CPPUnit-Tutorial,

Wikipedia

sowie Sourceforge.

Das CppUnit-Paket für Windows XP wurde bis etwa 2008 weitergepflegt. Die gegenwärtig (Mitte 2012) in dem oben angegebenen Repository von sourceforge zugängliche Version von CppUnit ist nicht ohne erhebliche Umbauten kompilier- und linkbar.

Anhand des Project-Files ist es als eine Version für Visual Studio 6 erkennbar, jedoch lässt es sich auf einem aktuellen Rechner mit Studio 6 nicht linken (Das Kompilieren geht). Die auftauchenden Probleme betreffen die Link-Reihenfolge, es werden Objekte entweder aus der Standardlibrary oder aus dem CppUnit-Paket nicht aufgelöst (Linkerfehler: nicht aufgelöstes externes Symbol) oder sie werden doppelt definiert. Versuche, die Linkreihenfolge anzupassen oder andere Vorgehensweisen, wie beispielsweise bestimmte Standardlibraries zu ignorieren (durch die ein Symbol mehrfach definiert wird), schlagen alle fehl.

Es scheint so zu sein, dass nicht nur die Studioversion, sondern auch andere Dateien (DLLs, Libs) des Betriebssystems selbst mit hineinspielen - noch in den Jahren 2007/2008 war dasselbe Paket nach unserer Erfahrung ohne Probleme kompilier- und linkbar, wie auch die Makros ohne Probleme angewendet werden konnten.

Einzige Abhilfe brachte eine stufenweise Umwandlung des Project-Files von Studio 6 über 8 auf 10. Zwar mußte auch diese Version für das Visual Studio 10 geringfügig angepaßt werden, indem die aus dem Studio 8 auf 10 konvertierte Projektdatei cppunit.sln mit einer funktionsfähigen *.sln-Datei aus Studio 10 verglichen wurde. Danach war das Projekt ausführbar. Allerdings scheint auch hier die Linkerreihenfolge eine extrem empfindliche Angelegenheit zu sein: eine auf einem Computer A erstellte cppunit.lib muß nicht unbedingt auf Computer B selbst unter Verwendung derselben Studioversion lauffähig sein.

CppUnit bietet eine Reihe von Makros, die Testfälle und -suiten möglichst einfach definieren lassen sollen. Dabei können die Suiten auch untergliedert werden. Die Makros werden bislang ausschließlich in dem Headerfile include\cppunit\extensions\HelperMacros.h erläutert, nicht aber in den Tutorials im Internet.

6.1. Unverzichtbare Makros

CPPUNIT_TEST_SUITE()
CPPUNIT_TEST()
CPPUNIT_TEST_SUITE_END()

Mit diesen Makros werden das jeweilige Testmodul, z.B. CDivisionTest, und ihre Methoden, z.B. TestDivision (x,y) dem CppUnit bekannt gemacht, sonst würden sie beim Testdurchlauf nicht berücksichtigt. Die Testmethode sollte im Übrigen am günstigsten als Makro mit einem call- Aufruf verpackt werden, z.B. hier als CALL_TESTDIVISION. Die Methoden bzw. Methodenmakros müssen in der Klassendeklaration von CDivisionTest auch mit aufgeführt werden. Die Namen werden ohne Anführungszeichen angegeben, am Schluss muss die Testsuite beendet werden:

CPPUNIT_TEST_SUITE(CDivisionTest)
CPPUNIT_TEST(CALL_TESTDIVISION)
CPPUNIT_TEST_SUITE_END()

Diese Makros CPPUNIT_TEST_SUITE, CPPUNIT_TEST und CPPUNIT_TEST_SUITE_END sind unverzichtbar, da nur mit ihnen die Testausführung automatisch abläuft. Außerdem muss die Testsuite aus dem selben Grund registriert werden. Das Makro CPPUNIT_TEST_SUITE_REGISTRATION(<Testmodul>) registriert eine Testsuite (z.B. "TEST_DIVISION") in einer globalen Variable, die keinen bestimmten Namen trägt, sondern mit dem fix vorgegebenen String "All tests" benannt wird. In den Erläuterungen im Headerfile HelperMakros.h wird das Übergabeargument für dieses Makro, eine Testklasse wie z.B. CoreTest, als ATestFixtureType bezeichnet, da die Testklasse immer von CPPUNIT_NS::TestFixture abgeleitet wird.

Alternativ kann das Makro CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(<Testmodul>, "Name der Testklasse") verwendet werden. Es registriert die Testsuite in einer globalen Variable unter einen bestimmten Namen.

6.2 Sonstige Makros

Weitere, aber nicht unbedingt notwendige Makros sind:

CPPUNIT_TEST_SUB_SUITE(): ermöglicht die Untergliederung von Suiten in Untersuiten.

CPPUNIT_TEST_SUITE_END_ABSTRACT(): registriert Templates für Testsuiten.

CPPUNIT_TEST_SUITE_PROPERTY(): Fügt Property Keys und Values mittels XML-files der Testsuite hinzu.

Im übrigen ist der Umfang an Tutorials dürftig. Im Code selbst gibt es einiges an Dokumentationen, doch ist nirgends eine vollständige Übersicht über die Architektur des CppUnit zu finden, geschweige denn Definitionen, wie "Test", "Testsuite", "Testfixture" etc. gegeneinander abgegrenzt sind. Der Aufbau des Codes ist einigermaßen komplex, zwar leidlich dokumentiert, aber auch hier machen die Kommentare genau da einen Punkt, wo es eigentlich interessant würde. Leider macht dies auch einen Einsatz der an sich sinnvollen Makros schwierig, da sie zumindest unter Visual Studio 10 Express nicht alle wie beschrieben funktionieren.

Praktisch ist das Makro CPPUNIT_ASSERT (bool). Allerdings bricht CppUnit bei einem Ergebnis des Vergleichs = false sofort ab, die Testfunktion würde nicht mehr zu Ende geführt. Deshalb ist es ratsam, den Variablenvergleich außerhalb des Makros zu realisieren, der das Ergebnis in einen Boole'schen Wert schreibt. Der Ist-Wert sowie das Ergebnis PASS oder FAIL wird in eine Ausgabedatei geschrieben und danach der Boole'sche Wert an CPPUNIT_ASSERT weitergereicht, das dann je nachdem den Test fortführt oder abbricht. Auf diese Weise ist gesichert, dass bei einem Abbruch des Tests der Fehlwert noch mit ausgegeben wird, was für das Debuggen meiner Ansicht nach sehr hilfreich ist.

Die Makros CPPUNIT_TEST_SUITE und CPPUNIT_TEST, mit denen die Testsuite erstellt werden soll, lassen sich zwar kompilieren und linken, jedoch wird kein Test ausgeführt, da die intern erstellte Testsuite nur ein leeres Element enthält. Verwendung anderer Makros führt regelmäßig zu massiven Kompilerfehlern (angeblich fehlende Strichpunkte, fehlende Typenspezifizierer). Sie können pragmatisch aufgelöst werden, indem die in ihnen enthaltenen Codezeilen direkt verwendet werden. Es ist ohnehin vorteilhafter, direkt auf die von CppUnit zur Verfügung gestellten Klassen zuzugreifen, da so besser kontrolliert werden kann, was geschieht.

CppUnit beendet bei erwartungsgemäßem Durchlauf das Programm mit Exitcode 0x0, im anderen Fall (wegen einer absichtlich nicht abgefangenen Exception) mit 0x1. Letztlich ist CppUnit dafür gedacht, eine Abfolge von Tests für ein oder mehrere Module in einer *exe-Datei zusammenzufassen, die auf Knopfdruck komplett durchläuft und ein Gesamtergebnis 0x0 oder 0x1 herausgibt. Dadurch lässt sich dieser Test gut in einen größeren Automatisierungsrahmen, z. B. mit JENKINS o.ä. einhängen. //FG

Literaturhinweise:

[1] PERRY, W. E. (2003): Software testen. mitp-Verlag, Bonn, 976 S., 1. Auflage 2003.

[2] PERRY, W. E, & RICE, R. W. (2002): Die 10 goldenen Regeln des Software-Testens. mitp-Verlag, Bonn, 230 S., 1. Auflage 2002.

[3] RODEN, G. (2010): Vom Saulus zum Paulus. Warum Unit-Tests sinnvoll sind. In: Dotnetpro 12/2010, S. 22 - 23.

[4] SPILLNER, A. & LINZ, T. (2005): Basiswissen Softwaretest. Aus- und Weiterbildung zum Certified Tester, Foundation Level nach ISQTB-Standard. dpunkt-Verlag, Heidelberg, 3. Auflage 2005.

* * Dr. rer. nat. Richard H. Kölbl ... ist beim Spezialisten für Systems Engineering, Project Resources und Consulting Mixed Mode tätig.

(ID:34878470)