image

Image

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 Standardbibliothek
für automatisierte Tests in Java

Michael Tamm

Image

Michael Tamm

mail@michaeltamm.de

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

Image   Für Alexandra und Niklas   Image

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: mail@michaeltamm.de

Michael Tamm
Berlin, 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:

Image

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

Diese Metapher – grüner Balken = alle Tests erfolgreich; roter Balken = ein oder mehrere Tests sind fehlgeschlagen – wurde bei der Integration von JUnit in alle Java-IDEs beibehalten. Auch in Eclipse, IntelliJ IDEA oder Netbeans sieht man heutzutage entweder einen grünen oder einen roten Balken, je nachdem, ob alle Tests erfolgreich ausgeführt wurden oder nicht.

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:

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.

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

Image

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 equalstrimWhitespace"a""a"