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