JUnit-Profiwissen - Michael Tamm - E-Book

JUnit-Profiwissen E-Book

Michael Tamm

0,0

Beschreibung

JUnit ist die Standardbibliothek zum Schreiben automatisierter Tests in Java. Dieses Buch enthält Grundlagen- und Expertenwissen für das effiziente Entwickeln automatisierter Tests in Java mit JUnit. Es vermittelt einen kompakten Überblick über alle Features von JUnit 3.8.1 bis JUnit 4.11 und zeigt anhand von Beispielen aus Tests bekannter Open-Source-Projekte, wann Sie diese sinnvoll einsetzen können. Darüber hinaus behandelt der Autor Open-Source-Bibliotheken wie Mockito und FEST, die das Schreiben von JUnit-Tests erleichtern, und verdeutlicht Programmierregeln für das Schreiben richtig "guter" Tests. Im Einzelnen werden behandelt: - Testgetriebene Entwicklung - Assertion-Bibliotheken - Unit-Tests mit Mock-Objekten - Programmieren gut verständlicher Tests - Programmieren schneller Tests - Tests abseits vom Happy Path - Nichtfunktionale Tests Abgerundet wird das Buch durch Hinweise und Tipps, wie Sie JUnit effektiv zusammen mit den bekannten Java-IDEs Eclipse und IntelliJ IDEA sowie mit den Build-Tools Ant und Maven einsetzen können.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 491

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Michael Tamm ist seit seinem Informatikstudium 1999 als Java-Programmierer und Systemarchitekt tätig und hat seitdem zahlreiche Enterprise-Webauftritte realisiert. Als Systemarchitekt ist er veranwortlich für gutes Softwaredesign, sauberen Code und schnelle Build-Prozesse. Über die Jahre hat er sich auf die Automatisierung von qualitätsichernden Maßnahmen wie beispielsweise Codereviews und Testen spezialisiert. Zudem veröffentlicht er gelegentlich Fachartikel in Computermagazinen, hält regelmäßig Vorträge auf diversen IT-Konferenzen und ist Committer der Open-Source-Projekte Selenium, Fighting Layout Bugs und JUnit Toolbox.

JUnit-Profiwissen

Effizientes Arbeiten mit der Standardbibliothekfür automatisierte Tests in Java

Michael Tamm

Michael Tamm

[email protected]

Lektorat: Christa Preisendanz

Copy-Editing: Friederike Daenecke, Zülpich

Herstellung: Birgit Bäuerlein

Umschlaggestaltung: Helmut Kraus, www.exclam.de

Druck und Bindung: M.P. Media-Print Informationstechnologie GmbH, 33100 Paderborn

Bibliografische Information der Deutschen Nationalbibliothek

Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;

detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.

ISBN

Buch 978-3-86490-020-4

PDF 978-3-86491-409-6

ePub 978-3-86491-410-2

1. Auflage 2013

Copyright © 2013 dpunkt.verlag GmbH

Wieblinger Weg 17

69123 Heidelberg

Die vorliegende Publikation ist urheberrechtlich geschützt. Alle Rechte vorbehalten. Die Verwendung der Texte und Abbildungen, auch auszugsweise, ist ohne die schriftliche Zustimmung des Verlags urheberrechtswidrig und daher strafbar. Dies gilt insbesondere für die Vervielfältigung, Übersetzung oder die Verwendung in elektronischen Systemen.

Es wird darauf hingewiesen, dass die im Buch verwendeten Soft- und Hardware-Bezeichnungen sowie Markennamen und Produktbezeichnungen der jeweiligen Firmen im Allgemeinen warenzeichen-, marken- oder patentrechtlichem Schutz unterliegen.

Alle Angaben und Programme in diesem Buch wurden mit größter Sorgfalt kontrolliert. Weder Autor noch Verlag können jedoch für Schäden haftbar gemacht werden, die in Zusammenhang mit der Verwendung dieses Buches stehen.

5 4 3 2 1 0

   Für Alexandra und Niklas   

Vorwort

JUnit ist ein »ganz alter Hut« und die Standardbibliothek zum Schreiben automatisierter Tests in Java. Ich gehe davon aus, dass die meisten Java-Programmierer bei ihrer täglichen Arbeit JUnit-Tests schreiben. Was also, werden Sie sich vielleicht fragen, soll dieses Buch über eine Bibliothek, die Sie wahrscheinlich tagtäglich selbst einsetzen?

Ich benutze JUnit seit inzwischen über 10 Jahren und dachte deshalb noch vor einiger Zeit, dass ich mich ganz gut mit dieser Bibliothek auskenne. Trotzdem habe ich in den letzten zwei Jahren – sehr zu meiner eigenen Überraschung – noch viel Neues über JUnit dazugelernt. Und genau das ist der Grund, warum ich dieses Buch geschrieben habe und denke, dass auch Sie es lesen sollten. Oder wissen Sie bereits, was es mit dem Theories-Runner auf sich hat? Oder wie Sie testübergreifende Aspekte elegant in eigene TestRule-Klassen auslagern können? Oder vielleicht wie Sie einzelne Testmethoden mittels der @Category-Annotation verschiedenen Testgruppen zuordnen und anschließend nur Tests einer bestimmten Gruppe ausführen können?

All dies sind recht neue und leider auch recht unbekannte Features von JUnit. Der Grund hierfür ist, dass die letzte Major-Version von JUnit – die Version 4.0 – bereits im Jahr 2006 veröffentlicht wurde und dass über die vielen Verbesserungen und neuen Möglichkeiten, die in den letzten Jahren im Laufe mehrerer Minor-Versionen zu JUnit hinzugekommen sind, nur sehr wenig berichtet wurde. Nach der Lektüre dieses Buches werden Sie einen guten Überblick über alle Features haben, die JUnit 4.11 bietet, und Sie werden wissen, wann Sie diese sinnvoll einsetzen können.

Neben diesen Features gibt es noch einige weitere Open-Source-Bibliotheken – zum Beispiel Mockito und FEST Fluent Assertions –, die das Schreiben von JUnit-Tests erleichtern und die ich deshalb sehr schätze und Ihnen mit diesem Buch nahebringen möchte.

Da die Kenntnis der JUnit-API allein nicht ausreicht, um gut verständliche, leicht wartbare, stabile und schnelle Tests zu schreiben, soll dieses Buch gleichzeitig auch ein Ratgeber dafür sein. Hierzu finden Sie in allen Kapiteln mehrere einfache Regeln, die ich auch immer durch Beispiele veranschauliche. Oft stammen diese Beispiele aus Tests bekannter Open-Source-Projekte.

Apropos Beispiele: Sie werden fast auf jeder Seite etwas Quellcode finden, da ich denke, dass Quellcode das beste Kommunikationsmedium ist, wenn es ums Programmieren geht. Dieses Buch ist kein akademisches Werk, sondern ein Buch für Praktiker.

Und da sich JUnit nicht nur zum Schreiben von Unit-Tests eignet, gebe ich Ihnen auch Tipps für das Schreiben von Integrations-, Frontend-, Performance-, Stress- und Architekturtests. Gerade die Architekturtests (mit denen Sie automatisch überprüfen können, ob sich Ihr gesamter Quellcode bei voranschreitender Entwicklung immer noch an Ihre Architekturvorgaben hält) sind ein Thema, das mir besonders am Herzen liegt, da es recht unbekannt ist.

Abgerundet wird das Buch durch Hinweise und Tipps, wie Sie JUnit effektiv zusammen mit den bekannten Java-IDEs Eclipse und IntelliJ IDEA sowie zusammen mit den Build-Tools Ant und Maven einsetzen können.

Auch wenn das Schreiben dieses Buches – übrigens mein erstes Buch – anstrengender und zeitraubender war, als ich anfänglich dachte, bin ich doch sehr stolz auf das Ergebnis. Ich hoffe, Sie haben viel Spaß beim Lesen und können viel Neues lernen. Und falls Sie vielleicht irgendwelche Anmerkungen haben, freue ich mich natürlich über jede Art von Feedback, am besten einfach per E-Mail an: [email protected]

Michael TammBerlin, im August 2013

Vorkenntnisse

Dieses Buch ist für alle Java-Programmierer bestimmt, egal ob Sie noch nie etwas von JUnit gehört oder schon Erfahrung im Schreiben von Tests mit JUnit haben. Selbst wenn Sie schon sehr viele Tests mit JUnit programmiert haben, bin ich überzeugt davon, dass Sie trotzdem noch eine Menge lernen können, wenn Sie dieses Buch lesen.

Neben der Tatsache, dass Sie grundlegende Kenntnisse der Java-Programmierung haben sollten, gehe ich davon aus, dass Sie wissen, was eine Jar-Datei ist, und dass Sie Jar-Dateien von Open-Source-Projekten downloaden und zum Klassenpfad eines Java-Projekts hinzufügen können.

Falls Sie Ant + Ivy, Maven oder Gradle für automatische Builds benutzen, sollten Sie in der Lage sein, eine neue Abhängigkeit zu Ihrem Projekt hinzuzufügen (oder zumindest den Kollegen kennen, der Ihnen dabei helfen kann). Wann immer ich eine Open-Source-Bibliothek näher vorstelle, zeige ich Ihnen, welche Zeilen zu einer pom.xml-Datei hinzugefügt werden müssen, um die Bibliothek in ein Maven-Projekt einzubinden. Die dabei angegebenen Maven-Koordinaten (groupId, artifactId und version) können Sie natürlich ebenfalls für Ivy oder Gradle benutzen.

Inhaltsverzeichnis

1      Einführung

1.1          Automatisierte Tests

1.2          Der grüne Balken

1.3          Funktionale Tests

1.4          Nichtfunktionale Tests

2      JUnit 3

2.1          Testklassen

2.2          Testmethoden

2.3          Assertion-Methoden

2.4          Testfixtures

2.5          Testsuites

2.6          Zusammenfassung

3      JUnit 4

3.1          Testklassen und -methoden

3.2          Die @Test-Annotation

3.3          Assertion-Methoden

3.4          Testfixtures mit @Before- und @After-Methoden auf- und abbauen

3.5          @Rule und eigene Testaspekte

3.6          @RunWith, Parameterized und eigene Runner

3.7          Testsuites

3.8          Testtheorien

3.9          Testgruppen/Testkategorien

3.10        Tests überspringen/ignorieren

3.11        Zusammenfassung

4      Testgetriebene Entwicklung

4.1          Einmal rundherum

4.2          Einen roten Test schreiben

4.3          Den roten Test grün machen

4.4          Codereview und Refactoring

4.5          ATDD – der Kontext für TDD

4.6          Zusammenfassung

5      Assertion-Bibliotheken

5.1          Hamcrest einbinden

5.2          Ein Blick unter die Motorhaube von Hamcrest

5.3          Eigene Hamcrest-Matcher schreiben

5.4          FEST Fluent Assertions

5.5          Zusammenfassung

6      Unit-Tests mit Mock-Objekten

6.1          Terminologie

6.1.1    Dummy-Objekt

6.1.2    Pseudo-Objekt

6.1.3    Fake-Objekt

6.1.4    Stub-Objekt

6.1.5    Mock-Objekt

6.1.6    Spy-Objekt

6.2          Mock-Objekte selbst schreiben

6.3          jMock

6.4          EasyMock

6.5          Mockito

6.6          Umgang mit unerwarteten Methodenaufrufen

6.7          Mock-Objekte injizieren

6.8          Mocken statischer Methoden

6.9          PowerMock

6.10        Zusammenfassung

7      Programmieren gut verständlicher Tests

7.1          Organisation und Benennung von Testklassen

7.2          Benennung von Testmethoden

7.3          Setup-Methoden

7.4          Das Test Data Builder Pattern

7.5          Der AAA-Stil

7.6          Das Page Object Pattern

7.7          Assertion-Messages

7.8          Zusammenfassung

8      Programmieren schneller Tests

8.1          Tests schneller machen

8.2          Testfixtures schneller machen

8.3          Tests zusammenfassen

8.4          Das Shared Testfixture Pattern

8.5          Tests parallel ausführen

8.6          Schnelles Feedback durch optimierte Testreihenfolge

8.7          Zusammenfassung

9      Tests abseits vom Happy Path

9.1          Exceptions im Test auslösen

9.2          Testen von Logmeldungen

9.3          Testen von Ausgaben auf System.out bzw. System.err

9.4          Testen von System.exit

9.5          Testen von Exceptions

9.6          Zusammenfassung

10     Nichtfunktionale Tests

10.1        Performance-Tests

10.2        Stresstests

10.3        Randomized Testing

10.4        Architekturtests

10.5        Zusammenfassung

11     JUnit und Eclipse

11.1        Wizards zum Erstellen von Testklassen

11.2        JUnit-Tests mit Eclipse ausführen

11.3        Erweiterte JUnit-Unterstützung durch das MoreUnit-Plug-in

11.4        Testabdeckung visualisieren mit EclEmma

12     JUnit und IntelliJ IDEA

12.1        Erstellen von Testklassen

12.2        JUnit-Tests mit IntelliJ IDEA ausführen

12.3        Testabdeckung visualisieren mit IntelliJ IDEA Ultimate Edition

13     JUnit und Ant

13.1        Die Ant-Tasks junit und junitreport

13.2        Testabdeckung messen mit JaCoCo

14     JUnit und Maven

14.1        JUnit-Tests mit dem Surefire-Plug-in ausführen

14.2        Tests parallel ausführen

14.3        Die Reihenfolge steuern, in der Tests ausgeführt werden

14.4        Nur bestimmte Tests ausführen bzw. bestimmte Tests ausschließen

14.5        Ausführen und Debuggen einzelner Tests

14.6        Ausführen von Integrationstests mit dem Failsafe-Plug-in

14.7        Testabdeckung mit Cobertura oder JaCoCo messen

15     Schlusswort

        Literaturverzeichnis

        Index

1 Einführung

Ich kann mich noch sehr gut daran erinnern, wie ich vor über 20 Jahren angefangen habe, Programmieren zu lernen. Meine ersten Programmiersprachen waren Basic, Assembler, Pascal und C++. Damals war OOP (objektorientierte Programmierung) das, was man heutzutage als »the next big thing« bezeichnen würde. Und ich muss zugeben, nachdem ich gelernt hatte, wie man objektorientiert programmiert, konnte ich mir nicht mehr vorstellen, jemals wieder anders zu programmieren.

Zu dieser Zeit, Anfang der 90er-Jahre, kamen die besten Programmierumgebungen noch von Borland. Und ich kann mich auch noch genau erinnern, wie sehr ich mich über eine neue Version von Turbo Pascal gefreut habe, weil sie ein Feature einführte, das heutzutage in jeder IDE selbstverständlich ist: Syntax-Highlighting.

Es gibt nicht viele Features in IDEs, die das Programmieren sehr viel angenehmer machen und die man – nachdem man einmal in ihren Genuss gekommen ist – einfach nicht mehr missen möchte. Heute zähle ich neben dem Syntax-Highlighting außerdem noch dazu: Code-Completion, Code-Folding, Refactoring-Support, Hintergrund-Kompilierung, einen integrierten Debugger und die Möglichkeit, einen Test auf Tastendruck oder Mausklick ausführen zu können – alles Dinge, die mich als Programmierer um Größenordnungen produktiver sein lassen, als wenn ich nur einen Texteditor hätte.

Mit der testgetriebenen Programmierung (siehe Kap. 4) ist es bei mir ähnlich wie mit der objektorientierten Programmierung: Nachdem ich vor inzwischen über 10 Jahren gelernt habe, wie man Unit-Tests mit JUnit schreibt, konnte ich mir nicht mehr vorstellen, jemals wieder ohne Tests zu programmieren. Und genauso programmiere ich auch heute noch!

In meinen Augen ist das testgetriebene Programmieren eine genauso große Errungenschaft wie das objektorientierte Programmieren. Beide Vorgehensweisen sind aus der modernen Softwareentwicklung nicht mehr wegzudenken. Und während Java als Programmiersprache ein gutes Werkzeug ist, um objektorientiert zu programmieren, ist JUnit ein gutes Werkzeug, um testgetrieben zu programmieren.

Und genau um dieses Werkzeug dreht sich dieses Buch, das man auch als Handbuch für JUnit bezeichnen könnte. Ich hoffe, wenn Sie es lesen, sind Sie besser in der Lage, das vielseitige Werkzeug JUnit in verschiedenen Situationen ideal einzusetzen.

1.1 Automatisierte Tests

Im vorigen Jahrtausend, als die testgetriebene Programmierung noch nicht Mainstream war, war es stattdessen üblich, Programme (nachdem man sie endlich erfolgreich kompiliert hatte) einfach auszuführen und nachzuschauen, ob das, was man gerade programmiert hatte, auch tatsächlich so funktionierte, wie man es sich vorstellte.

Schön, wenn dem so war.

Aber falls nicht, dann war entweder eine Sitzung mit dem Debugger fällig oder man spickte seinen Quellcode mit diversen Logmeldungen und führte ihn noch einmal aus – in der Hoffnung, durch die ganzen Logausgaben den Programmfluss nachvollziehen zu können und so die Stelle zu finden, wo etwas schiefging.

Da man jedoch immer nur die Funktionalität manuell testete, an der man gerade programmiert hatte, konnte es leicht passieren, dass man aus Versehen etwas anderes kaputt machte, ohne dass es bemerkt wurde.

Auch heutzutage ist es nach wie vor üblich, seinen Code mit Logmeldungen zu versehen, damit man im Falle eines Fehlers hoffentlich in der Logdatei einen Hinweis darauf finden kann, was schiefgegangen ist. Neben Open-Source-Bibliotheken wie Log4J, Commons Logging oder Logback bietet sogar das JDK seit der Version 1.4 hierfür (neben System.out.println) ein Logging-Framework im Package java.util.logging.

Ebenfalls immer noch üblich ist es – sehr zu meinem Unverständnis –, Software manuell zu testen. Dabei ist das manuelle Testen fehleranfällig, langsam, teuer und (wenn man es wiederholt macht) ziemlich langweilig.

Schreibt man hingegen automatisierte Tests, so kann man jederzeit auf Knopfdruck reproduzierbar, schnell (im Vergleich zum manuellen Testen) und ohne die Fehlerquelle Mensch feststellen, dass die selbst entwickelte Software noch das macht, was sie soll. Zugegeben, die Fehlerquelle Mensch ist immer noch da, schließlich werden automatisierte Tests von Menschen programmiert, aber das stupide Ausführen von Testschritten und das Vergleichen von Ist-Werten mit Soll-Werten übernimmt bei einem automatisierten Test ein Computer.

Die entscheidende Idee dabei ist, dass ein automatisierter Test völlig autark laufen kann, ohne dass ein Mensch irgendwelche Ausgaben lesen und interpretieren muss. Der automatisierte Test muss dazu nicht nur den Programmcode ausführen, sondern zusätzlich noch entscheiden, ob das Programm auch richtig funktioniert. Dies wird mit sogenannten Assertions (auf Deutsch: Behauptungen) gemacht. Eine Assertion vergleicht typischerweise das Ergebnis eines Methodenaufrufs oder den Zustand eines Objekts mit einem vom Testautor definierten Erwartungswert. Der Einsatz von Mock-Objekten (siehe Kap. 6) erlaubt Ihnen außerdem auch das Verhalten des Codes zu überprüfen, also ob bestimmte Methoden überhaupt aufgerufen werden und ob dabei die richtigen Parameter übergeben werden.

Wird ein automatisierter Test ausgeführt, so gibt es nur zwei Möglichkeiten: Entweder er war erfolgreich (wenn kein Laufzeitfehler aufgetreten ist und alle Assertions wahr waren) oder eben nicht.

Schlägt ein automatisierter Test fehl, der mithilfe der Testbibliothek JUnit geschrieben wurde, so trifft JUnit noch die Unterscheidung zwischen Error (ein unerwarteter Laufzeitfehler ist aufgetreten) und Failure (eine Assertion war falsch). Aber in der Praxis ist diese Unterscheidung nicht besonders relevant. Wichtig ist nur, dass alle Ihre Tests erfolgreich sind.

1.2 Der grüne Balken

Als JUnit noch nicht von den Java-IDEs unterstützt wurde, beinhaltete es neben einer Klasse zum Ausführen von Tests auf der Kommandozeile auch noch eine kleine GUI:

Abb. 1–1 AWT TestRunner von JUnit 3.8.1 mit grünem Balken

Das Schöne an dieser einfachen GUI war: Egal, wie viele automatisierte Tests ausgeführt wurden, es war immer sofort erkennbar, ob alle Tests erfolgreich waren (grüner Balken) oder ob zumindest ein Test fehlgeschlagen war (roter Balken).

Und auch bei Builds auf Continuous-Integration-Servern spricht man kurz von grünen und roten Builds, wobei man mit einem »grünen Build« einen erfolgreichen Build bezeichnet und mit einem »roten Build« einen fehlgeschlagenen Build.

Dabei muss die Ursache für einen fehlgeschlagenen Build nicht unbedingt ein fehlgeschlagener Test sein. Auch Kompilierfehler oder andere Build-Fehler bewirken, dass der Build-Versuch mit der Farbe Rot markiert wird. Nur bis zum Ende erfolgreich durchgelaufene Builds bekommen die Farbe Grün.

1.3 Funktionale Tests

Bevor ich Ihnen in den folgenden Kapiteln erkläre, wie Sie mit JUnit automatisierte Tests schreiben können, möchte ich Ihnen zunächst noch kurz aufzeigen, welche Vielzahl von Testarten es gibt. Wenn Sie einen automatisierten Test schreiben, sollte Ihnen nämlich stets bewusst sein, welche Art von Test Sie gerade schreiben, da dies Auswirkungen darauf hat, welchen Testansatz Sie wählen sollten und wie die Testumgebung, die Sie für Ihren Test aufbauen, aussehen sollte.

Als Erstes kann man die Unterscheidung in funktionale Tests und nichtfunktionale Tests treffen. Mit einem funktionalen Test überprüfen Sie (wie der Name schon vermuten lässt), ob die von Ihnen entwickelte Software funktioniert, also ob Ihr Code das macht, was er machen soll.

Funktionale Tests lassen sich wiederum weiter unterteilen nach dem Umfang des getesteten Codes. Auf der untersten Ebene sind da die sogenannten Unit-Tests zu nennen. Ein Unit-Test überprüft das Funktionieren einer einzelnen Unit. Das kann in Java eine einzelne Klasse sein, viel öfter aber ist es sogar nur eine einzige Methode, manchmal auch eine Teilmenge der Methoden einer Klasse. Entscheidend bei einem Unit-Test ist, dass bei der Ausführung des Tests möglichst nur der Produktionscode der zu testenden Methode bzw. Klasse durchlaufen wird. (Der Begriff Produktionscode bezeichnet den Quellcode derjenigen Klassen, aus denen Ihre Applikation besteht, die also später tatsächlich auch ausgeliefert werden. Testcode ist hingegen der Code, der lediglich zum Testen Ihres Produktionscodes dient. Die durch Testcode erzeugten Klassen werden nicht ausgeliefert.)

Insbesondere sollten Unit-Tests nicht zu Festplatten-, Datenbank- oder anderen Netzwerkzugriffen führen. Um dies zu erreichen, werden die Abhängigkeiten der getesteten Methode bzw. Klasse typischerweise durch sogenannte Mock-Objekte ersetzt, was ausführlich in Kapitel 6 beschrieben wird.

Der Vorteil von korrekt programmierten Unit-Tests ist, dass sie sehr, sehr schnell sind. Ein Unit-Test sollte nur wenige Millisekunden dauern. Nur so ist es möglich, in großen Projekten mit Tausenden von Unit-Tests innerhalb weniger Minuten einen Build durchzuführen, der das gesamte Projekt kompiliert und alle Unit-Tests ausführt.

Da es aber nicht ausreicht, nur einzelne Methoden oder Klassen zu testen, sondern auch das Zusammenspiel mehrerer Klassen getestet werden sollte, gibt es auf der nächsten Ebene die sogenannten Integrationstests. In einem Integrationstest wird typischerweise auf den Einsatz von Mocking verzichtet und eine Testumgebung aufgebaut, in der alle nötigen Abhängigkeiten zur Verfügung stehen, wie beispielsweise ein Spring ApplicationContext und/oder eine Testdatenbank. Manchmal ist aber auch bei Integrationstests der Einsatz von Mocking nützlich, nämlich immer dann, wenn Sie Komponenten simulieren wollen, die explizit nicht mitgetestet werden sollen, wie beispielsweise ein externer Webservice.

Dabei wird im Testcode von Integrationstests (genauso wie bei Unit-Tests) direkt auf die Klassen des Produktionscodes zugegriffen. Integrationstests sind also sogenannte Whitebox-Tests.

Sowohl Unit- als auch Integrationstests werden meistens von Entwicklern im Rahmen der testgetriebenen Entwicklung (siehe Kap. 4) geschrieben, um sicherzustellen, dass der gerade implementierte Produktionscode auch tatsächlich so funktioniert wie gedacht. Das ist bei den Tests auf der nächsthöheren Ebene – den sogenannten Szenariotests – anders: Szenariotests werden typischerweise nicht während der Entwicklung geschrieben, sondern hoffentlich davor, manchmal auch parallel (wenn Sie beispielsweise einen Test-Ingenieur in Ihrem Team haben), selten danach. Mit einem Szenariotest wird nicht eine bestimmte Klasse oder ein bestimmtes Modul getestet, sondern es wird ein kompletter Use Case durchgespielt.

Ein Szenariotest kann ebenfalls als Whitebox-Test geschrieben werden – oder aber als Blackbox-Test. Das heißt, er spricht die zu testende Applikation nur über solche Schnittstellen an, die auch einem Benutzer zur Verfügung stehen. Dann hätten Sie einen Szenariotest, der gleichzeitig auch ein Systemtest ist. Wenn Sie beispielsweise eine Desktop- oder Mobile-Applikation entwickeln, sollten Ihre Systemtests die Applikation über die Bedienoberfläche kontrollieren. Falls Sie eine Webapplikation programmieren, sollten Ihre Systemtests einen Webbrowser fernsteuern. Man spricht in diesen Fällen auch von GUI-, Frontend- oder Webtests. Entwickeln Sie hingegen einen Webservice, dann sollten Ihre Systemtests diesen über HTTP-Requests testen.

Während Unit-Tests auf der untersten Ebene angesiedelt sind, da sie nur sehr wenig Produktionscode testen, sind Systemtests auf der höchsten Ebene zu finden, da diese den Code aller beteiligten Komponenten testen. Während ein Szenariotest (wenn er als Whitebox-Test geschrieben wurde) nur Ihre Applikationslogik testet, testet ein Systemtest auch Ihren Frontendcode mit. Dieser muss dabei gar nicht in Java geschrieben sein. Im Falle einer Webapplikation könnte der Frontendcode beispielsweise in *.jsp- oder *.jsf-Dateien sowie in JavaScript-Dateien stecken.

Dabei sollten Sie Ihre Systemtests immer in einer Testumgebung laufen lassen, die möglichst nah an der Produktivumgebung ist. Während zum Beispiel bei Integrationstests aus Performance-Gründen gerne In-Memory-Datenbanken eingesetzt werden, sollten Sie bei Systemtests darauf achten, dass die Testumgebung genau das gleiche Betriebssystem, das gleiche JDK und die gleiche Datenbank verwendet wie die Produktivumgebung, und zwar jeweils in der gleichen Version. Falls Sie verschiedene Betriebssysteme, Datenbanken, Webbrowser oder was auch immer unterstützen wollen, sollten Sie für jede mögliche Kombination eine Testumgebung haben, in der Sie Ihre Systemtests laufen lassen.

Neben den bisher genannten funktionalen Testarten, die sich gut anhand des Umfang des getesteten Codes unterscheiden lassen, gibt es noch drei besondere Arten funktionaler Tests, die sich schlecht in dieses Unterscheidungsschema pressen lassen, die ich hier aber trotzdem erwähnen möchte:

Ein Szenariotest, wenn er als Blackbox-Test geschrieben wurde, ist gleichzeitig auch ein Systemtest. Um die Sache noch komplizierter zu machen, könnte ein solcher Test zusätzlich auch noch ein

Akzeptanztest

sein.      Einen Akzeptanztest zeichnet aus, dass er die Akzeptanzkriterien einer Programmieraufgabe testet. Egal wie diese Aufgabe konkret aussieht – es könnte beispielsweise eine Anforderung, eine User Story, eine Task Card oder ein Bug-Report sein. Der entscheidende Punkt ist: Jede Programmieraufgabe sollte klar definierte Akzeptanzkriterien haben, deren Erfüllung bedeutet, dass die Aufgabe erledigt ist.      Und wenn Sie immer automatisierte Akzeptanztests schreiben, die direkt aus den Akzeptanzkriterien abgeleitet sind, dann können Sie jederzeit sicher sein, dass Ihre Applikation alle bisher implementierten Anforderungen immer noch erfüllt, wenn diese Tests alle grün sind. Ein schönes Gefühl, das Sie nicht mehr missen möchten, wenn Sie es einmal erlebt haben. Ich komme auf das Thema Akzeptanztests noch einmal im letzten Abschnitt von

Kapitel 4

zurück, wo es um ATDD (

Acceptance Test Driven Development

) geht.

Als Nächstes sind die sogenannten

Regressionstests

zu nennen. Mit

Regression

wird die Art von Fehler bezeichnet, wenn etwas bereits einmal funktioniert hat, aber in einer neueren Version Ihrer Software plötzlich nicht mehr geht. Im weiteren Sinne sind eigentlich alle Tests immer auch Regressionstests, da sie ja verhindern sollen, dass irgendetwas plötzlich kaputt ist. Im engeren Sinne verstehe ich unter einem automatisierten Regressionstest jedoch einen Test, der explizit dafür geschrieben wird, das Auftreten einer Regression zu verhindern.      Hierfür ein Beispiel: Angenommen, Sie benutzen Java-Serialisierung, um Objekte in Ihrer Anwendung zu speichern. Ein typischer Unit-Test dafür sieht so aus, dass ein Objekt erzeugt, danach serialisiert und schließlich wieder deserialisiert wird. Anschließend wird überprüft, ob das deserialisierte Objekt die gleichen Eigenschaften hat wie das ursprünglich erzeugte. Nun stellen Sie sich vor, Sie fügen ein weiteres Feld zu der Klasse des Objekts hinzu. Dadurch wird der Unit-Test wahrscheinlich nicht rot werden. Aber wenn Ihre Anwendung im Livebetrieb versucht, eine alte Version zu deserialisieren, die das Feld noch nicht hatte, kommt es sehr wahrscheinlich zu einem Laufzeitfehler. Um dies zu verhindern, sollten Sie, sobald Sie Serialisierung einsetzen, immer auch einen Regressionstest schreiben, der versucht, eine alte serialisierte Version eines Objekts, die als Binärdaten vorliegt, zu deserialisieren.      Ein weiteres gutes Beispiel für Regressionstests sind

Migrationstests

: Wenn Ihre Applikation beispielsweise eine Datenbank verwendet, benötigen Sie eine Strategie dafür, wie Sie mit Schemaänderungen umgehen – also zum Beispiel wenn eine zusätzliche Spalte zu einer Tabelle hinzugefügt werden muss.      Eine mögliche Strategie ist der Einsatz von Open-Source-Bibliotheken wie Liquibase

1

oder flyway

2

, die gegebenenfalls Migrationsskripte ausführen, um die Datenbank auf den neusten Stand zu bringen. Wenn Sie diese Strategie verfolgen, dann sollten Sie immer auch einen Migrationstest schreiben, der diese Migrationsskripte testet und wie folgt funktionieren könnte:      Zunächst erzeugt der Test eine Testdatenbank, die mit einem möglichst alten Schema initialisiert wird, zum Beispiel durch ein SQL-Skript, das den Zustand des Schemas am Anfang der Entwicklung widerspiegelt. Danach werden alle Migrationsskripte ausgeführt. Tritt dabei keine Exception auf, ist das schon einmal ein gutes Zeichen dafür, dass die Migrationsskripte syntaktisch richtig sind. Wenn Sie anschließend noch das Schema der Testdatenbank mit dem erwarteten Schema vergleichen, können Sie sogar überprüfen, ob die Migrationsskripte auch inhaltlich richtig sind. Dafür benötigen Sie natürlich einen Mechanismus, mit dem Sie einfach an das erwartete Schema herankommen. Vielleicht pflegen Sie ja in Ihrem Produktionscode ein SQL-Skript zum Initialisieren einer leeren Datenbank oder das von Ihnen verwendete Persistenzframework bietet hierfür etwas an.      Wie auch immer: Mit Regressionstests testen Sie die Abwärtskompatibilität Ihrer Anwendung, und dazu benutzt ein Regressionstest immer irgendwelche Testdaten, die von einem älteren Stand Ihrer Software erzeugt wurden.

Die letzte Testart, die ich Ihnen hier vorstellen möchte, sind die sogenannten

Learning-Tests

. Wenn Sie eine neue Bibliothek in Ihrem Projekt einsetzen, auf eine externe Schnittstelle (wie beispielsweise einen Webservice) zugreifen müssen oder vielleicht auch nur eine API des JDK benutzen wollen, mit der Sie vorher noch nie zu tun hatten, müssen Sie als Erstes lernen, wie die neue Bibliothek, externe Schnittstelle oder API funktioniert. Hoffentlich steht Ihnen dafür irgendeine Dokumentation zur Verfügung, die Sie lesen können. Oder noch besser: Vielleicht gibt es sogar Beispielcode, auf dem Sie aufbauen können.      Aber weder Dokumentation noch Beispielcode können Ihnen garantieren, dass Sie alles richtig verstanden haben und sich die Bibliothek, externe Schnittstelle oder API auch so verhält, wie Sie es erwarten. Deshalb sollten Sie in einem solchen Fall, während Sie die Dokumentation lesen und lernen, immer auch gleich ein paar automatisierte Learning-Tests mit JUnit schreiben, die beweisen, dass die Third-Party-Komponente in Ihrer Laufzeitumgebung auch tatsächlich so funktioniert, wie Sie sich das vorstellen.      Ein automatisierter Learning-Test hat mehrere Vorteile gegenüber dem sofortigen Einsatz einer neuen Third-Party-Komponente im Produktionscode: Zum einen können Sie die neue Komponente in Isolation testen, und wenn Sie dabei ein Problem feststellen, wissen Sie, dass es nicht an Ihrem Produktionscode liegt. Darüber hinaus haben Sie sofort einen JUnit-Test, den Sie an einen Bug-Report anhängen können, falls Sie den festgestellten Fehler in den Bug-Tracker des Herstellers bzw. Open-Source-Projekts eintragen.      Sie sollten die Learning-Tests übrigens nicht wegwerfen. Auch wenn Learning-Tests nicht von Ihrem Continuous-Integration-Server ausgeführt werden sollten – schließlich testen Sie ja damit Third-Party-Komponenten, die außerhalb Ihrer Kontrolle sind –, sollten Sie die Learning-Tests trotzdem in Ihre Versionskontrolle einchecken. So können Sie später einmal, zum Beispiel falls von einer Open-Source-Bibliothek eine neue Version erscheint, ganz einfach durch das nochmalige Ausführen Ihrer Learning-Tests feststellen, ob Ihre Anwendung auch mit dieser neuen Version noch funktioniert. Oder Sie können, falls ein externer Webservice plötzlich Probleme macht, ebenfalls einfach die Learning-Tests noch einmal ausführen, um die Fehlerursache schnell einzukreisen.      Ihre Learning-Tests sind sozusagen Ihre ausführbare Dokumentation der Third-Party-Komponente. Und sollte die Dokumentation einmal von der Realität abweichen (was leider gar nicht so selten vorkommt, zum Beispiel wenn die Dokumentation veraltet ist), können Sie das jederzeit durch das Ausführen Ihrer Learning-Tests feststellen.

1.4 Nichtfunktionale Tests

Auch wenn Sie viel Aufwand in funktionale Tests gesteckt haben, ein funktionierendes Programm ist noch lange kein gutes Programm. Software sollte außerdem schnell, ressourcenschonend, stabil, sicher, fehlertolerant sowie benutzerfreundlich sein und darüber hinaus auch noch ansprechend aussehen. Und genau diese Aspekte können durch nichtfunktionale Tests überprüft werden.

Dies müssen keine manuellen Tests sein, JUnit eignet sich auch zum Schreiben von automatisierten nichtfunktionalen Tests: Ob Ihr Programm schnell ist und vor allem, dass es mit der Zeit nicht langsamer wird, können Sie mithilfe automatisierter Performance-Tests prüfen. Und ob Ihre Applikation ressourcenschonend und stabil ist, lässt sich durch automatisierte Lasttests bzw. Stresstests überprüfen. Zwar gibt es hierfür auch spezielle Tools, wie beispielsweise das bekannte JMeter3. Aber der Vorteil von automatisierten Performance- und Stresstests mit JUnit ist, dass Sie nicht nur Ihre gesamte Applikation, sondern auch dedizierte Komponenten, Klassen oder Methoden testen können. Diesem Thema widmet sich das Kapitel 10.

Um zu gewährleisten, dass Ihre Software auch robust und fehlertolerant ist, sollten Sie nicht nur Tests für den sogenannten Happy Path schreiben, also für den Normalfall, sondern auch für mögliche Fehlerfälle. Am einfachsten ist es natürlich, wenn Ihre Methoden im Produktionscode Exceptions gar nicht erst fangen, sondern einfach weiterwerfen, Sie also die Fehlerbehandlung anderem Code überlassen. Auch wenn ich das generell für eine gute Strategie halte, haben Sie sicherlich ein paar Stellen in Ihrem Produktionscode, wo Sie Exceptions behandeln. Auch dieser Code sollte getestet werden. Wie das geht, erkläre ich Ihnen in Kapitel 9.

Für einige nichtfunktionale Qualitätsmerkmale, wie Sicherheit, Benutzerfreundlichkeit oder ansprechendes Aussehen, ist es sehr schwer, automatisierte Tests zu schreiben, aber zumindest für Benutzerfreundlichkeit und Aussehen gibt es Ansätze, wie beispielsweise die Open-Source-Projekte web-accessibility-testing4 und Fighting Layout Bugs5. Trotzdem werden solche Qualitätsmerkmale typischerweise durch manuelle Penetrationstests und Usability-Tests überprüft.

Über die nach außen hin sichtbaren Qualitätsmerkmale hinaus sollte Ihre Software aber auch über eine gewisse innere Qualität verfügen, das heißt, sie sollte zum Beispiel leicht skalierbar und wartbar sein. Dafür sollte Ihr Quellcode gut verständlich sein und sich an Ihre Coding-Konventionen halten. Klassen, Felder, Methoden, Parameter und Variablen sollten aussagekräftige Namen haben. Ihre Architektur sollte einfach sein und die bekannten Prinzipien der objektorientierten Programmierung befolgen, wie beispielsweise das Single-Responsibility-Prinzip, das Liskov’sche Substitutionsprinzip oder das Dependency-Inversion-Prinzip.

Zur Überprüfung dieser inneren Qualitätsmerkmale dienen für gewöhnlich manuelle Codereviews. Es gibt dafür aber auch zahlreiche kommerzielle sowie Open-Source-Tools, von denen ich einige in Abschnitt 4.4 nenne. Sie können jedoch auch JUnit benutzen, um sogenannte Architecture Conformance Tests (oder kurz: Architekturtests) zu schreiben, mit denen Sie überprüfen können, ob sich der Quellcode Ihrer Applikation auch bei fortschreitender Entwicklung an alle von Ihnen aufgestellten Architekturregeln hält. Mehr dazu erfahren Sie in Abschnitt 10.4.

http://www.liquibase.org/

http://flyway.googlecode.com/

http://jmeter.apache.org/

http://web-accessibility-testing.googlecode.com/

http://fighting-layout-bugs.googlecode.com/

2 JUnit 3

JUnit hat Anfang Oktober 1997 das Licht der Welt erblickt, als Kent Beck (einer der Mitbegründer des Extreme Programming) und Erich Gamma (einer der Autoren des Klassikers »Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software«) zusammen die ersten Zeilen programmierten, während sie sich auf einem Flug von Zürich nach Atlanta zur OOPSLA-Konferenz befanden.

Kent Beck hatte bereits Erfahrung mit dem Schreiben von Test-Frameworks, da er schon SUnit für die Programmiersprache Smalltalk entwickelt hatte. Das relativ simple Design von SUnit und JUnit wurde im Laufe der Zeit noch in viele andere Programmiersprachen übertragen, sodass Testbibliotheken wie CppUnit (für C++), NUnit (für die .NET-Platform) oder auch PHPUnit (für PHP) entstanden. All diese Test-Frameworks werden zusammen auch als xUnit-Familie bezeichnet.

Heutzutage ist JUnit de facto die Standardbibliothek zum Schreiben von automatisierten Tests in der Java-Welt. Obwohl die aktuelle Version von JUnit (beim Schreiben dieser Zeilen) die Version 4.11 ist, möchte ich Ihnen jedoch zunächst JUnit 3.8.1 vorstellen, die viele Jahre lang die verbreitetste Version von JUnit war und mehr als 1 Million Mal von der JUnit-Projektseite auf Sourceforge heruntergeladen wurde:1

Abb. 2–1 JUnit 3.8.1-Downloads von der Sourceforge-Projektseite

Der Einbruch im Jahr 2006 fällt mit der Veröffentlichung von JUnit 4.0 zusammen. Hierbei nicht berücksichtigt sind die zahlreichen Downloads aus anderen Quellen, wie beispielsweise dem zentralen Maven-Repository.

Sie mögen sich fragen, warum Sie sich mit einer veralteten Version von JUnit auseinandersetzen sollen. Das würde ich normalerweise auch nicht tun. Aber es ist nun einmal so, dass sehr viele Softwareprojekte nach wie vor eine große Anzahl von JUnit-3-Tests haben, und auch heute noch werden viele Tests im Stil von JUnit 3 entwickelt. Deshalb denke ich, es ist sinnvoll zu wissen, wie JUnit 3 funktioniert.

2.1 Testklassen

Zunächst möchte ich Ihnen einen Test aus der Testsuite des Spring-Frameworks2 zeigen. (Als Testsuite bezeichnet man eine Sammlung mehrerer Tests.) Das Spring-Framework ist eine sehr bekannte Open-Source-Bibliothek für die Entwicklung von Java-Enterprise-Applikationen. Die erste Version von Spring stammt aus dem Jahr 2002, weshalb sich noch einige JUnit-3-Tests in der Codebasis finden lassen, wie beispielsweise dieser hier:

public class StringUtilsTests extends TestCase {    public void testHasTextBlank() throws Exception {...}    public void testHasTextNullEmpty() throws Exception {...}    public void testHasTextValid() throws Exception {...}    public void testContainsWhitespace() throws Exception {        assertFalse(StringUtils.containsWhitespace(null));        assertFalse(StringUtils.containsWhitespace(""));        assertFalse(StringUtils.containsWhitespace("a"));        assertFalse(StringUtils.containsWhitespace("abc"));        assertTrue(StringUtils.containsWhitespace(" "));        assertTrue(StringUtils.containsWhitespace(" a"));        assertTrue(StringUtils.containsWhitespace("abc "));        assertTrue(StringUtils.containsWhitespace("a b"));        assertTrue(StringUtils.containsWhitespace("a  b"));}public void testTrimWhitespace() throws Exception {        assertEquals(null, StringUtils.trimWhitespace(null));        assertEquals("", StringUtils.trimWhitespace(""));        assertEquals("", StringUtils.trimWhitespace(" "));        assertEquals("", StringUtils.trimWhitespace("\t"));        assertEquals("a", StringUtils.trimWhitespace(" a"));        assertEquals("a", StringUtils.trimWhitespace("a "));        assertEquals("a", StringUtils.trimWhitespacece(" a b "));(" a "));        assertEquals("a b", StringUtils.trimWhitespa        assertEquals("a b  c", StringUtils.trimWhitespace(" a b  c "));}

Dies ist ein klassischer JUnit-3-Test, der offensichtlich verschiedene Methoden der Klasse StringUtils testet. Die Testklasse leitet von der Basisklasse junit.framework.TestCase ab, was typisch für JUnit-3-Tests ist: Will man einen JUnit-3-Test schreiben, so muss die Testklasse immer entweder direkt von TestCase abgeleitet werden oder von einer anderen Klasse, die wiederum direkt oder indirekt von TestCase abgeleitet ist.

So bringen beispielsweise viele Open-Source-Bibliotheken eigene, von Test-Case abgeleitete Testbasisklassen mit, von denen man wiederum eigene Testklassen ableiten soll, um einfacher eigene Tests schreiben zu können: Das Spring-Framework bietet zum Beispiel die Klasse AbstractTransactionalSpringContext-Tests, die dafür sorgt, dass alle während des Tests vorgenommenen Änderungen in der Datenbank am Ende des Tests wieder zurückgerollt werden.

Es ist auch nicht ungewöhnlich, in eigenen Projekten eine BaseTestCase oder ähnlich benannte Klasse zu haben, die von TestCase abgeleitet ist und als Basisklasse für alle eigenen Tests dient. Eine solche Testbasisklasse stellt typischerweise Hilfsmethoden zur Verfügung, die über Vererbung von allen eigenen Testklassen genutzt werden können.

So oder so, TestCase ist die Basisklasse aller JUnit-3-Tests.3 Der Name der Testklasse – StringUtilsTests – zeigt an, dass diese Klasse Tests für die Klasse StringUtils enthält. Allerdings ist der Name der Testklasse in diesem Beispiel unüblich: Gängige Konvention (sowohl für JUnit 3 als auch für JUnit 4) ist, dass man zum Benennen einer Testklasse einfach nur » Test« an den Namen der zu testenden Klasse anhängt. Die meisten Programmierer hätten die Testklasse deshalb StringUtilsTest genannt. Eine Klasse, die hinten » Tests« heißt, repräsentiert normalerweise eine Testsuite, also eine Testklasse, die mehrere andere Testklassen bündelt.

Weiterhin wird eine Testklasse üblicherweise im gleichen Package wie die zu testende Klasse angelegt. Hierdurch erhält die Testklasse Zugriff auf alle protected sowie nur im Package sichtbaren Felder und Methoden der zu testenden Klasse. Trotzdem ist es ebenfalls üblich, den sogenannten Produktionscode vom Testcode zu trennen: Beim Spring-Framework sowie bei den meisten Projekten, die Maven als Build-Tool benutzen, liegen zum Beispiel alle Java-Quelldateien für den Produktionscode im Verzeichnis src/main/java. Alle Java-Quelldateien für die Tests befinden sich hingegen im Verzeichnis src/test/java.

2.2 Testmethoden

Im Listing auf Seite 12 können Sie auch sehen, dass alle Methoden der Testklasse public sind, den Rückgabetyp void haben, alle Methodennamen mit dem Präfix » test« anfangen und die Methoden keinerlei Parameter haben. Genau dies sind die vier Eigenschaften, die eine Methode als sogenannte Testmethode auszeichnen: Jede public void-Methode (in einer von TestCase abgeleiteten Klasse), deren Name mit » test« beginnt und die keine Parameter hat, repräsentiert einen einzelnen Test.

Das heißt, eine Testklasse kann durchaus mehrere Tests enthalten, was in der Praxis auch gang und gäbe ist. Wird eine Testklasse von JUnit ausgeführt, so wird für jede erkannte Testmethode eine eigene Instanz der Testklasse erzeugt und anschließend die Testmethode für diese Instanz aufgerufen.

2.3 Assertion-Methoden

Die beiden im Listing gezeigten Testmethoden bestehen eigentlich nur aus Aufrufen von assertTrue, assertFalse und assertEquals. Diese (sowie einige weitere) sogenannten Assertion-Methoden stehen in jedem JUnit-3-Test durch die Ableitung von TestCase zur Verfügung.

Assertions gehören zu jedem automatisierten Test: Eine der grundlegenden Ideen von JUnit ist ja, dass automatisierte Tests immer nur entweder erfolgreich (grün) sein können oder eben nicht (rot). Hierzu wird in einem JUnit-Test immer etwas Produktionscode ausgeführt, also zum Beispiel:

StringUtils.containsWhitespace(null)

Anschließend wird mithilfe einer Assertion-Methode überprüft, ob der Produktionscode genau das macht, was von ihm erwartet wird. So wird zum Beispiel mit der Zeile

assertFalse(StringUtils.containsWhitespace(null));

zum Ausdruck gebracht, dass der Aufruf der Methode StringUtils.contains-Whitespace mit dem Parameter null den Wert false zurückliefern sollte, da der String null ja kein Whitespace enthält.

Die Zeile

assertTrue(StringUtils.containsWhitespace ("a b"));

hingegen drückt aus, dass der Aufruf der Methode StringUtils.containsWhitespace mit dem Parameter "a b" den Wert true zurückliefern sollte, schließlich enthält der übergebene String ja ein Leerzeichen.

Neben den beiden Assertion-Methoden assertFalse und assertTrue, die jeweils einen booleschen Ausdruck als Parameter erwarten, ist die wahrscheinlich am meisten genutzte Assertion-Methode von JUnit 3 die folgende:

assertEquals("a", StringUtils.trimWhitespace (" a "));

Die Assertion-Methode assertEquals vergleicht immer einen Erwartungswert mit einem durch den Test »berechneten« Wert. Wobei dies nicht unbedingt ein numerischer Wert sein muss, sondern ein beliebiges Objekt sein kann, das mit dem Erwartungswert durch Aufruf der Methode equals verglichen wird. Im obigen Beispiel bedeutet die Assertion, dass die Methode trimWhitespace den String "a" zurückliefern sollte, wenn sie mit dem Parameter "a" aufgerufen wird.

Allen Assertion-Methoden ist gemeinsam, dass – falls die Assertion fehlschlägt – die Assertion-Methode einen AssertionFailedError wirft, wodurch zum einen die aktuelle Testmethode unterbrochen wird. Zum anderen erkennt JUnit so, dass der Test fehlgeschlagen ist. Alle Testmethoden, die bei ihrer Ausführung keine Exception oder Error werfen, werden als erfolgreich betrachtet. Testmethoden, die hingegen nicht normal zurückkehren, sondern eine Exception oder Error werfen, sind für JUnit fehlgeschlagene Tests.

Um zu erfahren, welche Assertion-Methoden JUnit 3 noch bietet, empfiehlt sich ein Blick ins Javadoc von JUnit oder direkt in den Quellcode. Dabei werden Sie feststellen, dass die Klasse TestCase selbst gar keine Assertion-Methoden hat, sondern diese alle von der Klasse Assert erbt. Hier eine kurze Übersicht aller von JUnit 3 bereitgestellten Assertion-Methoden:

assertTrue stellt sicher, dass die übergebene Bedingung wahr ist.

Neben der assertTrue-Methode, die einfach nur einen boolean-Parameter hat, gibt es noch eine zweite assertTrue-Methode, die zusätzlich einen String-Parameter akzeptiert. Hier kann eine kurze Fehlermeldung für den AssertionFailedError übergeben werden, der geworfen wird, wenn die übergebene Bedingung nicht wahr sein sollte. Hierfür ein Beispiel (ebenfalls aus der im Abschnitt 2.1 vorgestellten StringUtilsTests-Klasse):

public void testDeleteAny() throws Exception {    String inString = "Able was I ere I saw Elba";    String res = StringUtils.deleteAny (inString, "I");    assertTrue("Result has no Is [" + res + "]", res.equals(                                                  "Able was ere saw Elba"));

Durch die zusätzliche Übergabe des Strings

"Result has no Is [" + res + "]"

kann man – falls die Assertion fehlschlägt – direkt an der Fehlermeldung des geworfenen AssertionFailedErrors erkennen, was das eigentliche Ergebnis des Aufrufs von deleteAny war. Wird hingegen die assertTrue-Methode mit nur einem Parameter verwendet, sieht man lediglich anhand des Stacktraces, welche Assertion fehlgeschlagen ist, erhält aber keine aussagekräftige Fehlermeldung.

assertFalse stellt sicher, dass die übergebene Bedingung falsch ist.

Auch hier (wie auch bei allen folgenden Assertion-Methoden) besteht die Möglichkeit, eine informative Fehlermeldung als ersten Parameter zu übergeben.

assertEquals stellt sicher, dass zwei Objekte oder primitive Werte (boolean, byte, char, short, int, long, float oder double) gleich sind. Wenn Sie genau hinschauen, dann sehen Sie, dass die assertEquals-Methoden für float und double einen zusätzlichen Parameter delta haben. Was hat es damit auf sich?

public void test_double_arithmetic() {    double sum = 0.1 + 0.2;    assertEquals(0.3, sum); // ... will fail}

Dieser Test schlägt fehl5:

junit.framework.AssertionFailedError: expected:<0.3> but was:<0.30000000000000004>

Deshalb sollten Sie (nicht nur im Testcode, sondern auch im Produktionscode) zwei Gleitkommazahlen wie folgt auf Gleichheit überprüfen:

private static final double EPSILON = 0.000000001;public void test_double_arithmetic() {    double sum = 0.1 + 0.2;    assertTrue(Math.abs(sum - 0.3) <= EPSILON);}

Dieser Test ist grün.

Die Konstante EPSILON stellt hierbei den größten hinnehmbaren Rundungsfehler dar. Und genau dieser Algorithmus wird auch von JUnit verwendet, wenn Sie beim Vergleich von Gleitkommazahlen die assertEquals-Methode mit dem zusätzlichen Parameter delta verwenden. Der Test hätte also auch so geschrieben werden können:

public void test_double_arithmetic() {   double sum = 0.1 + 0.2;   assertEquals(0.3, sum, EPSILON);}

Natürlich gibt es auch hier zu jeder der zuvor aufgeführten assertEquals-Methoden eine analoge Methode, die zusätzlich als ersten Parameter einen String mit einer informativen Fehlermeldung akzeptiert:

Neben den assertEquals-Methoden für primitive Werte sowie für Object, die Sie bereits gesehen haben, fällt beim genauen Betrachten obiger Tabelle auf, dass es noch eine weitere überladene Version von assertEquals gibt, die zwei String-Instanzen als Parameter definiert.

Der Vergleich von zwei Strings ist ein recht häufiger Anwendungsfall beim Schreiben von automatisierten Tests, weshalb JUnit hierfür spezielle Unterstützung bereitstellt. So wirft der Test

public void test_assertEquals_with_strings() {    assertEquals("JUnit is cool.", "JUnit is old."); // ... will fail}

nicht einfach nur einen AssertionFailedError, sondern:

junit.framework.ComparisonFailure: expected:<JUnit is [cool].> but was:                                                            <JUnit is [old].>

Die Klasse ComparisonFailure ist von der Klasse AssertionFailedError abgeleitet und ist speziell für fehlgeschlagene Stringvergleiche geschrieben worden: In der obigen Fehlermeldung sind die Stellen, an denen sich die beiden Strings unterscheiden, in eckige Klammern gesetzt, sodass der Unterschied sofort ins Auge springt.

Sollten die verglichenen Strings sehr lang sein, so hilft JUnit auch hier, wie der folgende Test zeigt:

public void test_assertEquals_with_long_strings() {    final String quote = "MartinFowler about JUnit:\n" +                         "Never in the field of software development\n" +                         "have so many owed so much to so few lines of code.";    final String changed = "Martin Fowler about JUnit:\n" +                           "Never in the field of programming\n" +                           "have so many owed so much to so few lines of code.";    assertEquals(quote, changed); // ... will fail}

Dieser Test schlägt mit folgendem Fehler fehl:

junit.framework.ComparisonFailure: expected:<...ver in the field of                                                       [software development]have so many owed s...> but was:<...ver in the field of [programming]have so many owed s...>

Auch hier ist der Unterschied sofort sichtbar, da er wiederum durch eckige Klammern hervorgehoben ist. Darüber hinaus sind zusätzlich das gemeinsame Präfix sowie das gemeinsame Suffix der verglichenen Strings abgeschnitten. Stellen Sie sich vor, Sie haben einen Test, der den Inhalt zweier XML-Dateien vergleicht, die beide Hunderte von Zeilen lang sind und eine kleine Abweichung (sagen wir, in Zeile 117) haben. Sie wären JUnit dankbar, dass es sofort den Unterschied anzeigt und nicht erst die ersten 116 identischen Zeilen. Noch besser können es nur noch Eclipse und IntelliJ IDEA, die sogar einen Diff-View anbieten.

Neben den wichtigen Assertion-Methoden assertTrue, assertFalse und assertEquals bietet JUnit 3 aber noch ein paar weitere:

assertNotSame stellt sicher, dass zwei Objekte nicht identisch sind, bewirkt also genau das Gegenteil von assertSame.

assertNull stellt sicher, dass das übergebene Objekt null ist. Genauso wie bei assertSame gibt es auch zu dieser Assertion-Methode ein Gegenstück:

assertNotNull stellt sicher, dass das übergebene Objekt nicht null ist.

Damit kennen Sie nun alle Assertion-Methoden, die JUnit 3 bietet. Es sind nicht besonders viele, aber sie reichen für den Anfang aus.

Trotzdem vermisst man ziemlich schnell einige sinnvolle Assertion-Methoden, wenn man erst einmal damit angefangen hat, JUnit-Tests zu schreiben. Vielleicht haben Sie das sogar selbst schon erlebt. So fehlen in JUnit 3 zum Beispiel Assertion-Methoden, um Arrays auf Gleichheit zu überprüfen. Zwar kann man durchaus einen Test wie den folgenden schreiben:

public void test_assertEquals_with_arrays() {    int[] a1 = { 1, 2 };    int[] a2 = { 1, 2 };    assertEquals(a1, a2); // ... will fail}

Aber dieser Test schlägt leider fehl:

junit.framework.AssertionFailedError: expected:<[I@60aeb0> but was:<[I@16caf43>

Denn es wird die überladene assertEquals-Methode für Objekte verwendet, die die Gleichheit durch Aufruf der Methode equals ermittelt, was jedoch für Arrays in Java nicht funktioniert. Aus diesem Grund bringt übrigens JUnit 4 die Methode assertArrayEquals mit. Alternativ könnten Sie – falls in Ihrem Projekt JUnit 3 festgesetzt ist – auch assertTrue zusammen mit Arrays.deepEquals verwenden:

public void test_equality_of_two_dimensional_arrays() {    int[][] a1 = { { 1, 2 },                    { 3, 4 } };    int[][] a2 = { { 1, 2 },                    { 8, 4 } };    assertTrue(Arrays.deepEquals (a1, a2));}

Die Methode deepEquals der Klasse java.util.Arrays gibt es seit Java 5, um einen sogenannten »tiefen Vergleich« zweier Arrays durchzuführen. Das heißt, die einzelnen Elemente werden nicht einfach mithilfe von equals verglichen, sondern es wird rekursiv für alle Array-Elemente, die wiederum ein Array sind, der Inhalt dieser Arrays verglichen. Leider liefert dieser Workaround keine aussagekräftige Fehlermeldung, falls der Vergleich fehlschlägt – ganz im Gegensatz zu der assertArrayEquals-Methode von JUnit 4.

Aber auch andere Assertion-Methoden vermisst man gelegentlich, wie zum Beispiel eine assertNotEquals-Methode. (Hier gibt es eine gewisse Asymmetrie in JUnit 3: Zu allen assertXxx-Methoden gibt es entsprechende assertNotXxx-Methoden, nur nicht für assertEquals.) Ebenfalls wünschenswert wäre eine Assertion-Methode zum Überprüfen, ob zwei Dateien den gleichen Inhalt haben.

Genau diese Lücken werden übrigens von dem Open-Source-Projekt JUnitaddons6 gefüllt, das neben anderen nützlichen Dingen sowohl assertNotEquals als auch assertEquals für Dateien (in der Klasse FileAssert) mitbringt.

Zu guter Letzt möchte ich an dieser Stelle noch einmal betonen, dass Sie die in diesem Abschnitt gezeigten Assertion-Methoden von JUnit 3 zwar kennen sollten, aber für das Schreiben von Assertions nicht benutzen sollten, da es mehrere Open-Source-Bibliotheken gibt, die eine bessere Art und Weise des Schreibens von Assertions ermöglichen, wie zum Beispiel Hamcrest, FEST Fluent Assertions oder Truth (siehe Kap. 5). Und Hamcrest ist seit der JUnit-Version 4.4 sogar integraler Bestandteil von JUnit.

2.4 Testfixtures

Wie Sie bereits wissen, legt man beim Schreiben von Tests für eine beliebige Java-Klasse – nennen wir sie Foo – zunächst eine Testklasse namens FooTest an und fügt zu dieser Testklasse für jedes Feature von Foo eine Testmethode hinzu.

Beim Hinzufügen von Testmethoden zu einer Testklasse gelangt man sehr schnell an den Punkt, an dem man bemerkt, dass die verschiedenen Testmethoden oft einen ähnlichen Code enthalten: Um den zu testenden Produktionscode überhaupt erst ausführen zu können, muss immer der gleiche »Vorbereitungscode« geschrieben werden, der gewisse Java-Objekte erzeugt und initialisiert. Diesen Startzustand für den eigentlichen Testcode bezeichnet man als Testfixture.

Damit Ihre Testfixture nicht in jeder Testmethode immer wieder erzeugt werden muss, bietet es sich an, den »Vorbereitungscode« in eine eigene Methode auszulagern. Und genau dafür ist die setUp-Methode gedacht, die jeder JUnit-3-Test von der Basisklasse TestCase erbt. Die setUp-Methode wird von JUnit immer vor jeder einzelnen Testmethode ausgeführt. Sie kann in eigenen Testklassen überschrieben werden und ist der ideale Platz, um den Code zum Erzeugen der Testfixture aufzunehmen.

Auch hierfür möchte ich Ihnen wieder ein kleines Beispiel zeigen, diesmal aus der bekannten Open-Source-Bibliothek Commons IO7 des Apache Commons-Projekts. Das Apache Commons-Projekt ist ebenfalls schon recht alt, sodass es nicht verwundert, dass sich noch etliche JUnit-3-Tests im Sourcecode befinden. Es gliedert sich in mehrere Unterprojekte. Diese bieten für bestimmte Problemfelder, auf die man immer wieder beim Programmieren von Java-Applikationen trifft, Hilfsklassen und -methoden an, die das Standard-JDK vermissen lässt.

Das Problemfeld, das von Commons IO adressiert wird, ist die Ein- und Ausgabe. Hierfür wird beispielsweise die Klasse AutoCloseInputStream bereitgestellt, die einen beliebigen InputStream wrappt und ihn sofort schließt, sobald er vollständig gelesen wurde. So wird eine schnellstmögliche Freigabe von Ressourcen gewährleistet, und man kann sich sogar vor unsauber geschriebenem Code schützen, der einen InputStream nicht schließt – ein klassischer Java-Programmierfehler.

Natürlich gibt es für diese Klasse auch einen Test:

public class AutoCloseInputStreamTest extends TestCase {    private byte[] data;    private InputStream stream;    private boolean closed;    @Override    protected void setUp() {        data = new byte[] { 'x', 'y', 'z' };        stream = new AutoCloseInputStream(new ByteArrayInputStream(data) {            @Override            public void close() {                closed = true;            }        });        closed = false;    }    /**...*/    public void testClose() throws IOException {        stream.close();        assertTrue("closed", closed);        assertEquals("read()", -1, stream.read());    }    /**...*/    public void testRead() throws IOException {        for (byte element : data) {            assertEquals("read()", element, stream.read());            assertFalse("closed", closed);        }        assertEquals("read()", -1, stream.read());        assertTrue("closed", closed);    }    /**...*/    public void testReadBuffer() throws IOException {...}    /**...*/    public void testReadBufferOffsetLength() throws IOException {...}}

Die setUp-Methode stellt die Testfixture in den drei Membervariablen data, stream und closed zur Verfügung: Das Byte-Array data wird mit den ASCII-Codes der Buchstaben 'x', 'y' und 'z' initialisiert. Die Membervariable stream enthält den zu testenden AutoCloseInputStream, der einen ByteArrayInputStream wrappt, der wiederum die Bytes aus dem Array data ausliest. Außerdem ist die close-Methode des gewrappten ByteArrayInputStream so überschrieben, dass dem Feld closed der Wert true zugewiesen wird, sobald sie aufgerufen wird. Und zum Schluss setzt die setUp-Methode das Feld closed noch explizit auf false.

Im ersten Test, testClose(), wird überprüft, dass der Aufruf der close-Methode des AutoCloseInputStream an den gewrappten InputStream delegiert wird.

Der zweite Test, testRead(), überprüft die eigentliche von AutoCloseInput-Stream bereitgestellte Funktionalität, nämlich dass von dem AutoCloseInputStream genauso wie von dem gewrappten InputStream gelesen werden kann und dass – sobald beim Aufruf der read-Methode das Ende des Streams erreicht wurde – der gewrappte Stream automatisch geschlossen wurde.

Die letzten beiden Tests ähneln testRead() und sind im Listing deshalb nicht aufgeklappt. Sie unterscheiden sich dahingehend, dass dort zum Auslesen nicht die Methode read() ohne Parameter aufgerufen wird (die jeweils nur ein Byte liest), sondern die read-Methoden, die als Parameter ein Byte-Array akzeptieren, in das die gelesenen Daten geschrieben werden.

Alle diese Tests arbeiten mit der gleichen Testfixture, da JUnit immer zuerst die Methode setUp() vor der eigentlichen Testmethode aufruft.

Manchmal muss eine Testfixture nicht nur erzeugt, sondern nach einem Test auch wieder ordentlich abgebaut werden. Das ist zum Beispiel dann der Fall, wenn in der setUp-Methode eine Datenbankverbindung hergestellt wurde, die wieder geschlossen werden muss, oder wenn ein temporäres Verzeichnis erstellt wurde, das wieder gelöscht werden soll.

Für solche Aufräumarbeiten ist die Methode tearDown() da, die jede JUnit-3-Testklasse – ebenso wie setUp() – von der Klasse TestCase erbt und die bei Bedarf überschrieben werden kann. Die tearDown-Methode wird von JUnit immer nach dem Ausführen einer Testmethode aufgerufen, unabhängig davon, ob der Test erfolgreich war oder nicht. Auch hierfür ein Beispiel aus Apache Commons IO:

public class LockableFileWriterTest extends FileBasedTestCase {    private File file;    private File lockDir;    private File lockFile;    private File altLockDir;    private File altLockFile;    public LockableFileWriterTest(String name) {...}    @Override    public void  setUp() {        file = new File(getTestDirectory(), "testlockfile");        lockDir = new File(System.getProperty("java.io.tmpdir"));        lockFile = new File(lockDir, file.getName() + ".lck");        altLockDir = getTestDirectory();        altLockFile = new File(altLockDir, file.getName() + ".lck");    }    @Override    public void  tearDown() {        file.delete();        lockFile.delete();        altLockFile.delete();    }

Es folgen noch diverse Testmethoden für verschiedene Nutzungsszenarien von LockableFileWriter – einer FileWriter-Implementierung, die das gleichzeitige Schreiben in ein und dieselbe Datei durch verschiedene Prozesse verhindert.

Hierfür wird versucht, eine Lockdatei mit dem gleichen Namen wie die zu schreibende Datei plus der zusätzlichen Endung ".lck" anzulegen, und nur wenn dies gelingt, wird der Schreibzugriff erlaubt. Die Lockdatei wird beim Schließen des LockableFileWriter wieder gelöscht. Standardmäßig wird die Lockdatei in dem durch die System-Property java.io.tmpdir bestimmten temporären Verzeichnis angelegt, was in der durch die setUp-Methode aufgebauten Testfixture durch die Membervariablen lockDir und lockFile widergespiegelt wird.

Man kann beim Erzeugen eines LockableFileWriter-Objekts aber auch ein alternatives Verzeichnis für die Lockdatei angeben, was die Membervariablen altLockDir und altLockFile erklärt, die zum Testen dieses Szenarios dienen.

Beim Aufbau der Testfixture in der setUp-Methode wird außerdem zweimal die Methode getTestDirectory() aufgerufen, die von der Basisklasse FileBasedTestCase geerbt ist und immer das gleiche Testverzeichnis zurückgibt.

Die tearDown-Methode stellt sicher, dass alle in der Testfixture bereitgestellten Dateien am Ende jedes Tests wieder gelöscht werden.

Auch wenn dieses Beispiel die Verwendung von setUp() und tearDown() schön demonstriert, ist es leider kein besonders gutes Beispiel:8

1. Der Test ist nicht besonders gut verständlich, wenn man das Verhalten der Basisklasse FileBasedTestCase (in diesem Fall das Verhalten der getTestDirec-tory-Methode) nicht kennt. Um einen Test komplett zu verstehen, muss man nicht nur die Testklasse selbst anschauen, sondern immer auch alle Basisklassen, weshalb ich kein Fan von Testbasisklassen bin (siehe auch Abschnitt 7.3).

2. Die setUp-Methode macht mehr, als für einzelne Tests nötig wäre: Auch wenn die einzelnen Tests hier nicht abgedruckt sind, der erste Test testFileLocked() benutzt weder altLockDir noch altLockFile, da er das Szenario mit dem Standardverzeichnis für die Lockdatei testet. Der zweite Test, testAlternateLockDir(), benutzt hingegen weder lockDir noch lockFile, da er das Szenario mit dem alternativen Verzeichnis für die Lockdatei testet.

Obwohl die Methoden setUp() und tearDown() relativ schnell ausgeführt werden, summieren sich auch wenige Millisekunden bei einem großen Projekt mit Tausenden von Tests zu etlichen Sekunden unnötiger Laufzeit eines Builds. Außerdem muss (im dem Fall, dass ein einziger Test bricht) der ganze Code in setUp() und tearDown() gelesen und verstanden werden, und da führt jede unnötige Zeile Code zu schlechterer Wartbarkeit.

3. In der setUp-Methode wird nicht sichergestellt, dass die durch die Testfixture bereitgestellten Lockdateien auch tatsächlich nicht existieren, was jedoch Voraussetzung für den Erfolg der verschiedenen Testmethoden ist. Sie mögen jetzt vielleicht denken, dass dies nicht nötig ist, da die Lockdateien ja immer in der tearDown-Methode wieder gelöscht werden, aber das ist leider ein Denkfehler: Auch wenn JUnit sicherstellt, dass die tearDown-Methode unter allen Umständen nach jeder Testmethode aufgerufen wird, heißt das nicht, dass dies in Wirklichkeit auch immer passiert.

Stellen Sie sich vor, Sie haben einen gebrochenen Test und durchlaufen den Code mit dem Debugger, um zu verstehen, was schiefläuft. Plötzlich kommen Sie an die Stelle, wo der Fehler im Quellcode steckt. Voller Freude beenden Sie die Debugging-Session, um den Fehler zu fixen und den Test erneut auszuführen. Und genau jetzt haben Sie den Salat: Beim Abbrechen der Debugging-Session haben Sie die JVM gekillt und dadurch verhindert, dass die tearDown-Methode noch aufgerufen wird. Für das obige Beispiel würde das bedeuten, dass (obwohl Sie den Fehler gefunden und gefixt haben) die Tests trotzdem rot sind, da die Lockdateien noch herumliegen. Aus diesem Grund sollten Sie alle Vorbedingungen für Ihre Tests in der setUp-Methode herstellen.

4. Die setUp-Methode ruft nicht super.setUp() auf, und die tearDown-Methode ruft nicht super.tearDown() auf. Das muss auch nicht unbedingt gemacht werden, da die Methoden in TestCase – der Mutterklasse aller JUnit-3-Testklassen – leer sind. Aber dieser Test leitet ja von der Klasse FileBasedTestCase ab. Was ist, wenn dort bereits die setUp-Methode überschrieben wurde?

Weil es wirklich wichtig ist, möchte ich mich an dieser Stelle einmal wiederholen: Wenn Sie eigene setUp- oder tearDown-Methoden schreiben, beherzigen Sie bitte immer folgende zwei Regeln:

Wenn Sie die Methode setUp() überschreiben, rufen Sie als Erstes immer super.setUp() auf.

Sie sollten dies auch tun, wenn Sie direkt von TestCase ableiten. Vielleicht fragen Sie sich jetzt: »Warum, wenn doch die setUp-Methode in TestCase leer ist?« Die Antwort auf diese Frage lautet: »Damit Ihre Tests auch in Zukunft zuverlässig funktionieren.« Ich werde das gleich näher erläutern, aber zuvor noch die zweite Regel:

Wenn Sie die Methode tearDown() überschreiben, rufen Sie als Letztes immer super.tearDown() auf.

Diese beiden Regeln sind übrigens so wichtig, dass jede Verletzung einer dieser Regeln sogar von FindBugs9 moniert wird, einem bekannten Open-Source-Tool zur statischen Codeanalyse.

Und die zweite Regel sollten Sie ebenfalls auch dann befolgen, wenn Sie direkt von TestCase ableiten und der Aufruf von super.tearDown() eigentlich nichts bewirkt, da die tearDown-Methode in der Klasse TestCase leer ist. Um zu erklären, warum das trotzdem sinnvoll ist, möchte ich an dieser Stelle ein bisschen weiter ausholen.

Vielleicht kennen Sie das ja: In einem größeren Projekt schlagen immer mal wieder scheinbar zufällig Tests fehl, die beim nächsten Build – obwohl es keine Änderungen am Quellcode gab – wieder grün sind. Im Englischen spricht man von »Flaky Tests« oder »Non-Deterministic Tests«, ich nenne solche Tests »Wackeltests«. Das ist natürlich ein Verhalten, das man nicht haben will, insbesondere dann nicht, wenn ausgerechnet der eigene Commit zu einem roten Build führt und man für den gebrochenen Build verantwortlich gemacht wird, obwohl man gar nichts dafür kann.

Irgendwann, nach vielen Stunden Debugging und Kopfzerbrechen, fällt einem Programmierer auf, dass einige Tests Threads starten, diese jedoch nicht wieder beenden. Dies kann zu störenden, schwer nachvollziehbaren Seiteneffekten bei allen Tests führen, die danach ausgeführt werden: Schlägt ein nachfolgender Test fehl, weil ihm ein im Hintergrund weiterlaufender Thread aus einem vorigen Test in die Quere gekommen ist, dann ist es sehr schwer, die Ursache für das Fehlschlagen zu finden, weil man ja nur den roten Test vor Augen hat und an einen im Hintergrund laufenden Thread erst einmal gar nicht denkt. Damit so etwas in Zukunft schnell erkannt wird, schreibt der Programmierer die folgende Testbasisklasse:

public class BaseTestCase extends TestCase {    private Collection<Thread> liveThreadsBeforeSetUp;    @Override    protected void setUp() throws Exception {        liveThreadsBeforeSetUp = getLiveThreads();    }    @Override    protected void tearDown() throws Exception {        assertThatThereAreNoMoreLiveThreadsThanBeforeSetUp();    }    private Collection<Thread> getLiveThreads() {        return Thread.getAllStackTraces().keySet();    }    private void assertThatThereAreNoMoreLiveThreadsThanBeforeSetUp() throws                                                                  Exception {        final Set<Thread> liveThreads = new HashSet<Thread>(getLiveThreads());        liveThreads.removeAll(liveThreadsBeforeSetUp);        if (liveThreads.size() > 0) {            fail("Detected alive threads after execution of " + getName() +                                                       "(): " + liveThreads);        }    }}

Wird eine Testklasse von dieser Klasse abgeleitet, so wird nach jeder Testmethode sofort erkannt, ob noch neue Hintergrund-Threads laufen. Dazu werden in der setUp-Methode alle lebenden Threads bestimmt und in der privaten Membervariablen liveThreadsBeforeSetUp gespeichert. In der tearDown-Methode werden dann noch einmal alle lebenden Threads ermittelt, und anschließend wird überprüft, ob nach der Ausführung der aktuellen Testmethode neue Threads hinzugekommen sind. Sollte dies der Fall sein, wird durch den Aufruf der von JUnit bereitgestellten fail-Methode ein AssertionFailedError geworfen, was den Test, der für den überlebenden Hintergrund-Thread verantwortlich ist, mit einer dementsprechenden Fehlermeldung fehlschlagen lässt.

Beim Erzeugen der Fehlermeldung wird die ebenfalls von TestCase geerbte Methode getName() aufgerufen, die den Namen der aktuellen Testmethode zurückgibt.

Damit dieser Check nun auch tatsächlich bei allen bereits existierenden Tests greift, führt der Autor von BaseTestCase zusätzlich noch eine globale Suche und Ersetzung in allen Testklassen durch, wobei er » extends TestCase« durch » extends BaseTestCase« ersetzt.

Was wird nun passieren, falls ein anderer Programmierer sich nicht an die Regel gehalten hat, immer super.setUp() am Anfang einer eigenen setUp-Methode aufzurufen? Angenommen, er hätte eine Testklasse geschrieben, die direkt von TestCase ableitet, setUp() überschreibt, aber super.setUp() nicht aufruft und außerdem tearDown() nicht überschreibt – was keine seltene Konstellation beim Schreiben von JUnit-3-Tests ist.

Nach der globalen Ersetzung von » extends TestCase« durch » extends Base-TestCase« leitet diese Testklasse nun von BaseTestCase ab und wird nicht mehr funktionieren: Da die eigene setUp-Methode nicht super.setUp() aufruft, wird die Membervariable liveThreadsBeforeSetUp niemals initialisiert, was beim Aufruf von tearDown() zu folgendem Fehler führt:

java.lang.NullPointerException    at java.util.AbstractSet.removeAll(AbstractSet.java:152)    at BaseTestCase.assertThatThereAreNoMoreLiveThreadsThanBeforeSetUp(                                                        BaseTestCase.java:30)    at BaseTestCase.tearDown(BaseTestCase.java:21) <8 internal calls>

Dies ist – wie so oft bei NullPointerExceptions – leider keine besonders aussagekräftige Fehlermeldung. Folgende kleine Änderung an der Klasse BaseTestCase schafft Abhilfe:

private void assertThatThereAreNoMoreLiveThreadsThanBeforeSetUp() throws                                                                  Exception {    if (liveThreadsBeforeSetUp == null) {        fail (getClass().getName() + ".setUp() does not call super.setUp().");    }    ...

Lange Rede, kurzer Sinn: Vergessen Sie bitte nicht, in eigenen setUp-Methoden immer super.setUp() aufzurufen und in eigenen tearDown-Methoden immer super.tearDown(), damit Ihre Tests auch beim Einschieben einer neuen Testbasisklasse weiterhin funktionieren.

Am allerbesten ist es natürlich, wenn Sie setUp() überhaupt gar nicht erst überschreiben. Wie Sie dennoch elegant eine Testfixture bereitstellen können, zeige ich Ihnen in Abschnitt 7.3.

Um das gelegentliche Überschreiben von tearDown() werden Sie nicht herumkommen, wenn Sie JUnit 3 benutzen. Um hierbei sicherzustellen, dass super. tearDown() auch unter allen Umständen aufgerufen wird, reicht es nicht aus, den Aufruf einfach am Ende Ihrer eigenen tearDown-Methode zu platzieren: Sie sollten sicherstellen, dass super.tearDown() auch dann aufgerufen wird, wenn Ihr eigener Aufräumcode eine Exception wirft, weshalb eine überschriebene tearDown-Methode immer so aussehen sollte:

@Overridepublic void tearDown() throws Exception {    try {        ...    } finally {        super.tearDown();    }}

2.5 Testsuites

Sobald Sie mehrere Tests haben, möchten Sie sehr wahrscheinlich auch gelegentlich mehrere Tests auf einmal ausführen. Dies wird sowohl von allen bekannten Build-Tools (wie Ant, Maven oder Gradle) unterstützt als auch von IDEs (wie Eclipse, IntelliJ IDEA oder Netbeans).

Obwohl es sowohl mit Build-Tools als auch mit IDEs einfach ist, alle Tests eines Projekts oder alle Tests in einem bestimmten Package auszuführen, gibt es ab und zu Situationen, in denen man explizit nur eine ganz bestimmte Untermenge von Tests ausführen möchte, wobei diese gewünschte Untermenge nicht bzw. nur sehr schwer mit den Konfigurationsmöglichkeiten der Build-Tools bzw. IDEs spezifiziert werden kann.

Stellen Sie sich zum Beispiel ein großes Projekt mit Tausenden von Tests vor. Normalerweise sollen alle Entwickler vor einem Commit in die Versionskontrolle sicherstellen, dass der Build nicht gebrochen wird. Aber die Ausführung aller Tests dauert einfach viel zu lange. In dieser Situation wäre beispielsweise eine Testsuite SmokeTests10 wünschenswert, die nur die wichtigsten Tests enthält. Wenn man von den Entwicklern schon nicht verlangen kann, vor jedem Commit alle Tests auszuführen, dann doch wenigstens die wichtigsten.

Ein anderes Beispiel: Stellen Sie sich vor, Sie programmieren ein bestimmtes Feature in einem großen Projekt, natürlich testgetrieben (siehe Kap. 4). Da Sie bereits bestehende Klassen anfassen, wollen Sie im TDD-Zyklus allerdings nicht nur immer den roten Test ausführen, den Sie gerade geschrieben haben, sondern auch die bereits existierenden Tests für die Klassen, die Sie ändern, um sicherzustellen, dass Ihre Änderungen nichts kaputtmachen. Hierfür ist eine temporäre Testsuite ideal geeignet, die Sie zum Entwickeln schnell anlegen, aber nicht in die Versionskontrolle einchecken, sondern wieder löschen, wenn das Feature fertig programmiert ist.

Wie ich bereits in Abschnitt 2.1 angemerkt habe, bezeichnet der Begriff Testsuite zunächst einmal lediglich eine Ansammlung mehrerer Tests, die alle einmal durchlaufen werden, wenn die Testsuite ausgeführt wird. Damit kann zum Beispiel die Menge aller Testklassen gemeint sein, auf deren Namen einer der folgenden Wildcard-Ausdrücke passt:

<includes>    <include>**/Test*.java</include>    <include>**/*Test.java</include>    <include>**/*TestCase.java</include></includes>