Testen mit JUnit

Martin Kompf

Testen ist ein unverzichtbarer Bestandteil der Entwicklung von Software. Dabei empfiehlt es sich, schon sehr frühzeitig im Entwicklungsprozess mit der Erstellung von Tests zu beginnen. Idealerweise erfolgt die Programmierung einer Java Klasse und des zugehörigen Tests gleichzeitig. Ein wichtiges Hilfsmittel dabei ist JUnit.

JUnit 4

JUnit hat sich bald nach seinem Erscheinen als Standardwerkzeug für das Testen von Javaprogrammen etabliert. Die aktuelle Version ist JUnit 4; die Version JUnit 3 findet noch für ältere Software aus Kompatibilitätsgründen Verwendung. Die Popularität von JUnit hat zu einer Vielzahl von Derivaten für andere Programmiersprachen und Anwendungsgebiete geführt. Zum Beispiel CppUnit für das Testen von C++ Anwendungen, XMLUnit für den Vergleich von XML Daten oder DbUnit für den Umgang mit Datenbanken während des Tests.

Schon installiert

Dieser Artikel stellt im ersten Teil die Arbeit mit JUnit im Rahmen der Entwicklungsumgebung Eclipse für Java vor. Da JUnit bereits in Eclipse integriert ist, kann man sofort mit dem Programmieren von JUnit Tests beginnen.

Class under Test

Als erstes benötigt man natürlich ein zu testendes Object - die Class under Test (CuT). Im Rahmen dieser Einführung dient als Beispiel hierfür ein simpler Algorithmus zu Berechnung der Fakultät:

package de.kompf.testing.basic;

public class Calculator {
 public long nfac(int n) {
  if (n < 0) {
    throw new IllegalArgumentException(
      "Can't compute factorial of numbers < 0");
  }
  long result = 1;
  for (int i = 2; i <= n; ++i) {
    result *= i;
  }
  return result;
 }
}

Da die Methode nfac keinen Zugriff auf Attribute der Instanz Calculator benötigt, würde man sie normalerweise als static deklarieren. Dieses Beispiel verzichtet jedoch darauf, um später bei der Programmierung des Tests die Instanzierung der CuT demonstrieren zu können.

Organisation

Organisation des Workspace Als erstes sollte man sich Gedanken über die Organisation des Arbeitsbereiches machen. Es empfiehlt sich eine klare Trennung zwischen produktivem Sourcecode und Tests. Ein bewährter Stil ist, unterschiedliche Verzeichnise auf Projektebene zu verwenden: Der produktive Code steht unterhalb von src, während die JUnit Tests in test erstellt werden (siehe Abbildung). Dabei ist wichtig, das Verzeichnis test in Eclipse per New - Source folder anzulegen. So kompiliert es Eclipse beim Build automatisch mit. Bei der späteren Auslieferung des Codes können dann entsprechend konfigurierte Buildskripte das test Verzeichnis explizit von der Aufnahme in die Produktdistribution ausschließen.

Außerdem sollte man diesen Zeitpunkt gleich nutzen, um die JUnit Libraries mit in den Classpath des Projektes aufzunehmen. Das erfolgt auf Projektebene per Build Path - Add Libraries.... Im Dialog wählt man dann JUnit aus und selektiert die Version JUnit 4.

@Test

Der JUnit Test ist eine ganz normale Java Klasse, die durch Annotationen aus dem JUnit Framework angereichert ist. Der Klassenname sollte dabei einem festen Schema, wie Name der CuTTest folgen. Legt man den Test in das gleiche Package wie die CuT, dann hat er auch Zugriff auf Methoden und Instanzvariablen mit protected oder default Sichtbarkeit, was bei der Testprogrammierung eine große Hilfe sein kann. Wie oben ausgeführt, legt man das Package im Verzeichnis test an:

package de.kompf.testing.basic;

import static org.junit.Assert.*;
import org.junit.*;

public class CalculatorTest {

  private Calculator calc;

  @Before
  public void initCalculator() {
    calc = new Calculator();
  }

  @Test
  public void testNfac() {
    assertEquals(1, calc.nfac(0));
    assertEquals(1, calc.nfac(1));
    assertEquals(2, calc.nfac(2));
    assertEquals(3628800, calc.nfac(10));
    assertEquals(2432902008176640000L, calc.nfac(20));
  }
}

Die Testklasse besteht in diesem Fall nur aus einem einzigen Test, der durch die Annotation @Test gekennzeichet ist. Die Methode assertEquals entstammt der Klasse org.junit.Assert aus dem JUnit Framework. Sie überprüft die zwei übergebenen Argumente auf Gleichheit: Das erste Argument ist das erwartete Ergebnis, an zweiter Stelle steht der Aufruf der zu testenden Funktion mit dem entsprechenden Eingangswert. Stimmen erwartetes und berechnetes Ergebnis nicht überein, dann wird der Test mit einem AssertionError abgebrochen.

Die CuT ist im Beispiel als private Membervariable calc des Testklasse deklariert. Die Initialisierung der CuT erfolgt in der mit der Annotation @Before gekennzeichneten Methode. JUnit führt diese Methode automatisch vor jedem Test aus. Bei umfangreichen Initialisierungen und mehreren Tests kann man sich so jede Menge duplizierten Code sparen.
Die folgende Tabelle enthält eine Übersicht über die am meisten genutzten Annotationen:

Annotation Methodenspezifikation Verwendung
@Test public void Eine Testmethode
@Before public void Läuft vor jeder Testmethode
@After public void Läuft nach jeder Testmethode
@BeforeClass public static void Läuft einmal pro Testklasse vor der ersten Testmethode. Sinnvoll für aufwändige Initialisierungen.
@AfterClass public static void Läuft einmal pro Testklasse im Anschluss an alle Testmethoden. Hier kann man die in @BeforeClass allokierten Ressourcen freigeben.

Logischerweise kann man im Test nicht die Ergebnisse für alle möglichen Eingangsparameter überprüfen. Sinnvoll sind Tests an den Grenzen des erlaubten Parameterbereichs, wie 0, 1 und 2 sowie für einige weitere Werte.

Expected Exception

Aber auch der Fall der Übergabe ungültiger Parameter ist einen Test wert! Der Aufruf von nfac mit dem Parameter -1 sollte eine IllegalArgumentException produzieren - so sieht der entsprechende Test dafür aus:

  @Test(expected=IllegalArgumentException.class)
  public void testNfacIllegalArgument() {
    calc.nfac(-1);
  }

Der Parameter expected der @Test Annotation instruiert JUnit, das Auftreten der spezifizierten Exception als erfolgreichen Test zu werten.

Testrunner

Eclipse Testrunner Eclipse hat alle zum Ausführen von JUnit Tests notwendigen Werkzeuge an Bord. Per Rechtsklick auf die Testklasse im Packageexplorer und Run As - JUnit Test lässt sich der Test ausführen. Führt man den Rechtsklick auf übergeordneten Elementen, wie Packages oder Folder aus, kann man auch mehrere Tests mit einem Mal ausführen.

Das Ergebnis des Tests ist ein grüner oder roter Balken - auf diese Art und Weise ist eine sofortige Aussage über den Erfolg oder Misserfolg des Tests möglich. Eine Aussage kann daher ohne die manuelle Interpretation von Testprotokollen erfolgen - das ist eine ganz wichtige Eigenschaft und ein Vorteil des Konzepts von JUnit.

Die direkte Integration des Testrunners in die Eclipse IDE ermöglicht dem Programmierer das häufige Ausführen seiner JUnit Tests direkt während der Programmierung. Fehler werden so frühzeitig - noch vor dem Einchecken ins Versionskontrollsystem - erkannt. Andererseits möchte man aber die JUnit Tests auch automatisiert im Batch ausführen können, zum Beispiel als Bestandteil von Continuous Integration oder im Rahmen des Releasebuilds des Softwareproduktes. Eine Möglichkeit hierfür ist die Verwendung des Buildtools Ant.

Automatisierung

Die Vorstellung von Ant als unverselles Tool zur Automatiserung erfolgte bereits in Die ersten Schritte zur Java Programmierung. Für die Zwecke der Testautomatisierung enthält es die Tasks JUnit und JUnitReport.

Das Ant Buildfile build.xml beginnt man üblicherweise mit Definitionen oft verwendeter Pfade als property:

<project name="testing" default="all" basedir=".">
  <!-- The path to junit4.jar -->
  <property name="junit4.jar" value="/usr/share/java/junit4.jar"/>

  <property name="src.dir" value="${basedir}/src"/>
  <property name="build.dir" value="${basedir}/bin"/>
  <property name="test.dir" value="${basedir}/test"/>
  <property name="report.dir" value="${basedir}/reports"/>

Den Pfad zur JUnit 4 Library muss man eventuell anpassen. Unter Windows verweist man entweder auf das in Eclipse enthaltene JUnit, welches sich irgendwo unterhalb des plugins Verzeichnisses befindet. Oder man lädt sich das JAR File von JUnit.org.
Linux User haben es natürlich einfacher, hier steht JUnit als Bestandteil der Distribution zur Verfügung. Debian und Ubuntu Benutzer installieren JUnit 4 zum Beispiel mittels apt-get install junit4. Der Befehl dpkg -L junit4 gibt dann über die Pfade zu den installierten Dateien Auskunft.

Die restlichen Pfade sind relativ zum Projektverzeichnis spezifiziert. Sie funktionieren demzufolge nur dann, wenn build.xml direkt in diesem Verzeichnis liegt.

Nun erfolgt die Deklaration der Targets zum Kompilieren der JUnit Tests. Da Produktiv- und Testcode in unterschiedlichen Verzeichnissen stehen, bekommt der javac Task zwei src Pfade übergeben:

  <target name="init">
    <mkdir dir="${build.dir}"/>
    <mkdir dir="${report.dir}/html"/>
  </target>

  <target name="compile-test" depends="init" description="compile JUnit tests">
    <javac destdir="${build.dir}" includeantruntime="false">
      <src path="${src.dir}"/>
      <src path="${test.dir}"/>
      <classpath>
        <pathelement path="${junit4.jar}"/>
      </classpath>
    </javac>
  </target>

Das Ausführen der Tests übernimmt der junit Task. Die Spezifikation der Testklassen erfolgt hier im Element batchtest: Die Liste der Tests enthält somit alle unterhalb des Directory test stehenden Javadateien, deren Name auf Test endet:

  <target name="run-tests" depends="compile-test" description="Run the JUnit tests">
    <junit fork="yes" dir="${basedir}" printsummary="yes" haltonfailure="no">
      <classpath>
        <pathelement path="${junit4.jar}"/>
        <pathelement path="${build.dir}"/>
      </classpath>

      <formatter type="xml"/>

      <batchtest fork="yes" todir="${report.dir}">
        <fileset dir="${test.dir}" includes="**/*Test.java"/>
      </batchtest>
    </junit>
  </target>

Das Attribut printsummary instruiert den junit Task, eine kurze Zusammenfassung über die Anzahl der Tests und aufgetretenen Fehler auf die Standardausgabe zu schreiben. Außerdem produziert der formatter Subtask eine Reihe von XML Files mit detaillierten Testergebnisses. Der Task junitreport überführt diese in eine lesbare Form:

  <target name="createreports" depends="init"
        description="Create HTML reports of the test results">
    <junitreport todir="${report.dir}">
      <fileset dir="${report.dir}" includes="TEST-*.xml"/>
      <report format="noframes" todir="${report.dir}/html"/>
    </junitreport>
  </target>

  <target name="all" depends="run-tests, createreports"/>

</project>

Der Aufruf von ant im Projektverzeichnis führt somit automatisch alle Testklassen aus und erzeugt eine hübsche HTML Datei mit den Testergebnissen.

Fazit

JUnit 4 ist ein einfach zu benutzendes und leichtgewichtiges Framework für die Entwicklung von Softwaretests. Die Integrationen in Eclipse und Ant zeigen, dass eine Auführung einzelner Tests oder kompletter Testsuiten jederzeit im Entwicklungsprozess möglich ist. Nur das Programmieren der Tests kann JUnit naturgemäß nicht leisten, jedoch ist die Einstiegshürde für den erfahreren Javaentwickler minimal. Es ist dabei sehr zu empfehlen, möglichst frühzeitig mit der Erstellung von Tests zu beginnen! Sie sind der wichtigste Bestandteil der Qualitätssicherung. Insbesondere spätere Änderungen an der Software im Rahmen funktionaler Erweiterungen oder eines Refactorings sind ohne vorhandene Regressionstests oftmals gar nicht durchführbar.