Test-driven Development mit JUnit
Bei Entwicklung oder Änderung einer Software ist es wichtig sicherzustellen, dass der Programmcode exakt nach der Anforderungsspezifikation erstellt wurde und in allen möglichen Situationen und Fällen (Use Cases) korrekt funktioniert. Das erfolgt durch verschiedene Tests, mit denen auch der Programmcode auf Fehler geprüft wird. Entwickler benutzen Komponententests, um Funktionen, Schnittstellen, Benutzerinteraktion sowie Datenübertragung und -verarbeitung in den implementierten Modulen zu testen.
Die Komponententests gehören zum Entwicklungsprozess und sind ein fester Bestandteil der Qualitätssicherung. Für die Qualität des Codes spielen sowohl die Professionalität der Entwickler als auch die verwendeten Testmethoden und -tools eine wichtige Rolle. Menschliche Fehler passieren immer, wenn eine Arbeit gemacht wird. Die Kunst der Programmierung besteht darin, mögliche Fehler im Code rechtzeitig, noch im Rahmen des Entwicklungsprozesses und vor dem Wirkbetrieb zu identifizieren, zu lokalisieren und zu beheben. Dafür ist eine ausgeklügelte und systematische Testmethodik der Komponententests unerlässlich.
Softwaretests
Beim Testen eines Programms wird geprüft, ob das Programm die Anforderungen erfüllt, die vor dem Beginn der Entwicklung in Form eines Anforderungskatalogs und einer detaillierten Spezifikation zusammengestellt worden sind. Die Teststrategie ist es, Fehler in der implementierten Software zu finden. Sind keine Fehler bei einem Testlauf aufgetreten, bedeutet das noch nicht, dass die Software fehlerfrei ist. Denn Fehler könnten bei dem verwendeten konkreten Testszenario latent bleiben.
Bei einem vollständigen Test ist es wichtig alle Ablaufzweige, -bedingungen und -pfade abzudecken. Der Testerfolg besteht nicht darin, dass keine Fehler gefunden wurden, sondern im Identifizieren aller möglichen Fehler durch korrektes, systematisches und vollständiges Durchtesten der Software. Dafür sind wiederholte und auf Suche nach Fehlern ausgerichtete Tests erforderlich.
Es gibt verschiedene Arten von Tests
- Unit-Tests (Modultests, Komponententests)
- Systemtests (Tests eines Systems oder Teilsystems)
- Integrationstests (Tests des Gesamtsystems mit allen integrierten Softwarekomponenten und Teilsystemen)
- Einzeltests (zur Prüfung einzelner Funktionen/Szenarien)
- Regressionstests (Wiederholung der zuvor ausgeführten Tests, um zu prüfen, dass die Software nach Softwareänderungen korrekt funktioniert)
- Entwicklertests, Lasttests, Akzeptanztests, Sicherheitstests, Abnahmetests etc.
Tests werden entweder automatisch oder manuell ausgeführt. Generell unterteilt man alle Tests in zwei Kategorien:
- Blackbox Testing – Testen nur auf Basis der Anforderungen, ohne Heranziehen der Kenntnisse über die Programmstruktur und den Programmcode
- Whitebox Testing – Testszenarios und TestCases basieren auf der Kenntnis der Implementierungsdetails, wie z.B. bei Entwicklertests eines Programmierers
JUnit Tests: Was ist das?
JUnit (Java Unit) ist ein verbreitetes Framework, das sich zum Standardwerkzeug für automatisierte Unit-Tests von Klassen und Methoden in Java-Programmen etabliert hat. Das Konzept von JUnit basiert auf dem Framework SUnit, das für SmallTalk entwickelt wurde. Mittlerweile gibt es eine Reihe von ähnlichen Frameworks und Erweiterungen, die das Konzept von JUnit übernommen haben und Unit-Tests in anderen Programmiersprachen ermöglichen: NUnit (.Net), CppUnit (C++), DbUnit (Datenbanken), PHPUnit (PHP), HTTPUnit (Webentwicklung) u.a. Solche Frameworks für Unit-Tests werden allgemein als “xUnits” genannt.
Das JUnit-Framework wird ständig vom JUnit-Projekt weiterentwickelt und ist aktuell in der Version JUnit 5 als Open Source verfügbar. Im Rahmen von White-Box-Tests der Quellcode wird JUnit für Unit-/Modul-/Komponenten-Tests verwendet.
Im Unterschied zu einem vollständigen komplexen System sind Algorithmen auf Unit-Ebene meist übersichtlich und weisen eine geringere Komplexität auf. Da die Methoden eines Java-Programms über klar definierte Schnittstellen verfügen, können sie mit relativ wenigen Testfällen weitgehend vollständig getestet werden. Dies ermöglicht JUnit-Tests in den agilen Softwareentwicklungsprozess zu integrieren, Unit-Tests noch vor der funktionalen Entwicklung auf Basis der Anforderungen zu entwerfen (Extrem Programming, XP) und parallel mit der funktionalen Entwicklung anzuwenden (Qualitätssicherung).
Die modulspezifischen Use Cases lassen sich auf typische, repräsentative Stichproben wie “Normalproben” (normale Werte), “Extremproben” (maximale/minimale/Grenzwerte) und “Negativproben” (unzulässige Werte) beschränken, was die Anzahl der erforderlichen Testfälle drastisch reduziert. Soll die Interaktion mit externen Komponenten für einen Unit-Test erforderlich sein, wird diese Schnittstelle einfach simuliert.
Wie funktionieren JUnit Tests?
Das JUnit Framework ist in allen gängigen IDEs wie Eclipse, NetBeans, IntelliJ und BlueJ und Build-Tools wie Gradle, Maven und Ant integriert. Zu jeder Klasse im Projekt erstellst Du eine passende Testklasse, mit der das Verhalten bzw. die Methoden der jeweiligen Klasse getestet werden.
Mit JUnit 3.x leitest Du deine Tests von einer JUnit TestCase Klasse ab und implementierst nur die Testmethoden. Mit JUnit 4.x und 5.x (ab JDK1.5) musst Du kein JUnit Test erweitern und verwendest stattdessen Java Annotations. Hier einige wichtige Annotations:
@Test – Kennzeichnung einer Testmethode (public void)
@Before – wird vor jeder Testmethode ausgeführt (im Vorlauf, zum Initialisieren des Tests; public void)
@After – wird nach jeder Testmethode ausgeführt (im Nachlauf, zum Abräumen des Tests; public void)
@BeforeClass – wird einmal pro Testklasse vor der ersten Testmethode ausgeführt, z.B. für Initialisierungen (public static void)
@AfterClass – wird einmal pro Testklasse beim Beenden aller Tests ausgeführt, um z.B. die mit @BeforeClass allokierten Ressourcen freizugeben (public static void)
@Ignore – wird zusammen mit @Test verwendet, um den Test zu ignorieren, wenn er z.B. noch nicht implementiert ist
Der JUnit TestRunner führt den Test aus und protokolliert die Ergebnisse. Ein Test kann entweder ein einzelner TestCase sein oder zusammen mit anderen TestCases in einer TestSuite ausgeführt werden.
Namenskonvention
Analog zu Namensgebung für Objekte in deinem Java-Code sollst Du eine einheitliche Namenskonvention auch bei Benennung der Elemente des JUnit-Tests verwenden. Ein JUnit-Testprojekt, seine Klassen und Methoden haben direkten Bezug auf das zu testenden Projekt, die Klassen und Methoden. Damit diese Beziehung eindeutig und konsistent bleibt, sollst Du einer derselben Namenskonvention in deinen Projekten folgen. Am besten fügst Du den Originalnamen das Postfix “Test” hinzu, um Testelemente des Codes zu benennen und den Bezug auf die Originalelemente beizubehalten.
Zum Beispiel:
Originalprojekt: MyProject –> JUnit-Testprojekt: MyProject.Test
Originalklasse: MyClass –> Testklasse: MyClassTest
Originalmethode: MyMethod –> Testmethode: MyMethod_UseCaseID_test
Originalpackage: MyPackage –> Testpackage: MyPackageTest
Seit JUnit 4.x werden die Aktionen zur Initialisierung, Testausführung und -aufräumung durch Annotationen kenntlich gemacht und die Namen können frei gewählt werden. Obwohl die JUnit-Tests der Package-Struktur folgen, haben sie jedoch eine eigene Wurzel “Test” im Dateisystem, so dass das Build unabhängig von den TestCases der TestSuite läuft.
Was testet man mit JUnit ab?
JUnit-Tests sind die erste Bastion im Kampf gegen Software-Bugs. Dahinter stecken System-, Integrations-, Akzeptanz- und weitere Tests. Mit JUnit prüfen die Entwickler bzw. andere Tester die korrekte, d.h. fehlerfreie und anforderungskonforme Implementierung einzelner Module des Java-Codes. Jede nicht triviale Funktion oder Methode könnte Fehler enthalten und sollte mit JUnit getestet werden.
Sowohl ein neuer als auch ein geänderter Java-Code wird im LifeCycle mit JUnit getestet. Bei Konzeption, Planung, Implementierung und noch vor dem eigentlichen Testlauf mit JUnit den Aufwand und das Nutzen des Unit-Tests abwägen. Sicherheits- und Missionskritische Software-Systeme (Luft- und Raumfahrt, Medizin, Militär, Bahn- und Straßenverkehr u.a.) stellen besondere Qualitätsansprüche an die Stabilität, Zuverlässigkeit und Sicherheit der Software. Daher sollst Du möglichst “alles” in einem solchen sicherheitskritischen Java-Programm testen – auch Objekte und Methoden, die dir als “trivial” und “fehlerfrei” erscheinen. Denn Folgen eines im Code übersehenen bzw. nicht getesteten Fehlers können verheerend sein.
Du sollst Pre- und Post-Conditions überprüfen und sowohl Positive-Cases (Normalfälle, Sonderfälle) als auch Negative-Cases (Negativtests, Ausnahmen) ausführen. Dabei sollst Du gezielt Exceptions provozieren, um die Stabilität der Software zu prüfen. Erwartete Exceptions durch try-catch-Anweisung abfragen und bei unerwarteter Situation mit fail() abbrechen.
Jeder neuen oder geänderten Anforderung folgt ein neuer Test. Den Test sollst Du noch vor der Implementierung der Anforderung schreiben.
Eigenschaften und Besonderheiten von JUnit Tests
Für jede zu testende Methode erstellst Du mindestens eine Testmethode, in der die Ergebnisse der zu testenden Klasse/Methode validiert werden. Es werden Bedingungen definiert, die den Rückgabewert (Ist-Wert) der zu testenden Methode mit dem erwarteten Wert (Soll-Wert) vergleichen. Alternativ kann das Ergebnis auf Erfolg (true) geprüft werden. Das Testergebnis wird mit der JUnit-Methode assertEquals() bzw. assertTrue() ausgewertet.
Ein Test kann entweder “erfolgreich” (Grün) oder “nicht erfolgreich” (Rot) sein. Schlägt ein TestCase einmal fehl und liefert ein unerwartetes Ergebnis, bedeutet das entweder einen Bug im getesteten Java-Code oder einen Fehler der implementierten JUnit-Testmethode. In beiden Fällen sollst Du den Fehler korrigieren und den JUnit-Test wiederholen.
Die Bibliothek junit.jar muss für die IDE verfügbar sein. JUnit 5 kannst Du einem Java-Projekt in Eclipse wie folgt hinzufügen:
Project > Properties > Java Build Path > Libraries > Add Library... > JUnit 5 > Finish
Um einen Test auszuführen, kannst Du die Testklasse per Rechtsklick selektieren und im Kontextmenü das Item “Run As > JUnit Test” auswählen. Eclipse öffnet JUnit-Ansicht und zeigt Erfolg (Grün; Failures: 0) bzw. Misserfolg (Rot; Failures: >0).
Alternativen zu JUnit Test
NUnit, TestNG, Arquillian, Mockito und Cucumber sind die bekanntesten populären Alternativen zum JUnit.
Wie kann man JUnit beim Entwickeln nutzen?
JUnit ist in deiner Java IDE wie NetBeans oder Eclipse schon integriert. JUnit-Tests sind für automatische und wiederholte Unit-Tests (Regression-Tests) am besten geeignet. Du musst den Anfangs- und Endzustand, das erwartete Ergebnis und die Testmethoden eindeutig definieren. Du sollst darauf achten, dass der Abdeckungsgrad der Unit-Tests hoch ist (im Idealfall vollständig) und alle wichtigen UseCases und Konstellationen durchgetestet werden. Ein einmal entwickelter Test wird an die geänderten Anforderungen angepasst und nicht entfernt.
Angefangen von einzelnen Klassen und Methoden lassen sich Testhierarchien bis hin zum gesamten Java-Projekt aufbauen. Der Projekttest kann in das tägliche (daily) Master-Build integriert werden, um den aktuellen Stand des eigentlichen Entwicklungsprojekts und des JUnit-Projekts zu sichern. Der Test-First-Ansatz des Extreme Programming mit JUnit verbessert die Definition und Validierung der Schnittstellen sowie die Strukturierung und Übersichtlichkeit der Architektur.
Fazit
Das Ziel jedes Software-Tests ist es, Software-Bugs präventiv, noch vor Übergabe der Software in den Betrieb zu identifizieren. JUnit ermöglicht eine effiziente entwicklungsbegleitende Qualitätssicherung sowohl bei der Entwicklung einer neuen Java-Software als auch bei späteren Änderungen, Optimierungen und Erweiterungen des Codes im Lebenszyklus des Projekts. Mit dem JUnit Framework und den erstellten, im Code spezifizierten Testmethoden (TestCases, TestSuites) lassen sich die Unit-Tests nicht nur vom Autor des Java-Programms sondern auch von einem anderen Tester ausführen. Diese Vorgehensweise unterstützt das bekannte Vier-Augen-Prinzip und ermöglicht es, die vorhandenen Unit-Tests bei einem externen Code-Review (Audit) nicht nur automatisch auszuführen, sondern auch die Testergebnisse mit JUnit automatisch evaluieren zu lassen.
JUnit gilt als das bewährteste Framework zur Testautomation und dient als ein Standard-Testwerkzeug jeder professionellen Java-Entwicklung. JUnit unterstützt agile Unit-Tests nach dem Konzept des Test-driven Developments (TDD) und ist mithilfe von JUnit-Framework und IDE in den Entwicklungsprozess nahtlos integriert. JUnit-Tests erhöhen die Softwarequalität und werden zum festen Bestandteil der Qualitätssicherung im Lebenszyklus eines Java-Entwicklungsprojekts.