JUnit ist ein Java-Werkzeug, mit dem Java-Klassen automatisiert getestet werden können (ursprünglich von Kent Beck und Erich Gamma konzipiert).
Während bei Black-Box- und Akzeptanztests (sowie meistens auch bei System-, Integrations-, Performanz- und Lasttests) die Software als Ganzes und ohne Kenntnis der Internas getestet wird, wird JUnit im Java-Umfeld eingesetzt für Unit-, Komponenten- und White-Box-Tests, bei denen die Internas der zu testenden Komponente bekannt sind.
Zu den zu testenden Klassen werden Testklassen mit mehreren Testmethoden erzeugt. Mehrere Testklassen können zu Test-Suiten zusammengefasst werden. Das TestRunner-Tool führt die Tests aus und berichtet kummulativ die Ergebnisse.
Besonders moderne "agile" Vorgehensmodelle zum Softwareentwicklungsprozess (z.B. XP, Extreme Programming) werben mit "Test Driven Development" ("TDD") und erwarten extensives Testen und das Erstellen der Tests bevor mit der eigentlichen Kodierung begonnen wird.
Legen Sie ein Projektverzeichnis an, z.B.
'D:\Java\JUnit'.
Erzeugen Sie in diesem JUnit-Projektverzeichnis folgende Verzeichnisse:
- <projektverzeichnis>\classes
- <projektverzeichnis>\src
- <projektverzeichnis>\src\meinpackage
- <projektverzeichnis>\tst
- <projektverzeichnis>\tst\meinpackage
Die Testklassen sollten in einem getrennten Verzeichnisbaum (hier unter 'tst') abgelegt werden, damit sie später leicht weggelassen werden können. Aber die Package-Struktur des 'src'-Verzeichnisses sollte identisch auch im 'tst'-Testverzeichnis wiederholt werden, damit die Testklassen die zu testenden Klassen leichter aufrufen können.
Als zu testende Java-Klasse soll das folgende einfache Beispiel dienen,
welches von Zahlen wahlweise das Quadrat oder die Wurzel ermittelt.
Speichern Sie im Unterverzeichnis
'<MeinJUnitDir>\src\meinpackage'
die folgende Nutzklassendatei 'MeineKlasse.java':
package meinpackage;
public class MeineKlasse
{
private String job;
public String getJob() {
return job;
}
public void setJob( String job ) {
this.job = job;
}
public double myMethod( double x ) throws Exception
{
if( "Quadrat".equalsIgnoreCase( job ) )
return x * x;
if( "Wurzel".equalsIgnoreCase( job ) )
return Math.sqrt( x );
throw new Exception( "Fehler: Aufgabe nicht korrekt definiert." );
}
}
Speichern Sie im Unterverzeichnis '<MeinJUnitDir>\tst\meinpackage' die folgende Testklassendatei 'MeineKlasseTest.java':
package meinpackage;
import junit.framework.TestCase;
public class MeineKlasseTest extends TestCase
{
MeineKlasse meineKlasse1;
public void setUp() throws Exception
{
meineKlasse1 = new MeineKlasse();
assertEquals( "Anfangs darf kein Job gesetzt sein.",
null, meineKlasse1.getJob() );
}
public void tearDown() throws Exception
{
meineKlasse1 = null;
}
public void testGetAndSetJob()
{
meineKlasse1.setJob( "Quadrat" );
assertEquals( "Job muss 'Quadrat' sein.",
"Quadrat", meineKlasse1.getJob() );
}
public void testDoJobs() throws Exception
{
meineKlasse1.setJob( "Quadrat" );
assertTrue( "Quadrat von '4' muss '16' sein.",
16. == meineKlasse1.myMethod( 4 ) );
meineKlasse1.setJob( "Wurzel" );
assertTrue( "Wurzel von '4' muss '2' sein.",
2. == meineKlasse1.myMethod( 4 ) );
meineKlasse1.setJob( null );
try {
meineKlasse1.myMethod( 4 );
fail( "Exception muss geworfen werden, da kein korrekter Job gesetzt." );
} catch( Exception ex ) {}
}
}
In 'setUp()' können Initialisierungen durchgeführt werden.
In 'tearDown()' kann aufgeräumt werden (z.B. Datenbankverbindungen schließen).
Die Testmethoden 'test...()' müssen folgende Bedingungen erfüllen,
um (per Reflection) als Testmethoden erkannt zu werden:
- Der Name muss mit 'test' beginnen
- Sie dürfen keine Parameter haben
- Sie müssen 'public' und dürfen nicht 'static' sein
Doku zu den 'assert...()'- und 'fail()'-Methoden finden Sie in der Doku zur Klasse Assert. Beachten Sie die Vorgehensweise, wie mit 'fail()' im 'try'-Block das Werfen von Exceptions kontrolliert wird.
Diese Testklasse kann (nach Kompilierung, s.u.) bereits als JUnit-Test ausgeführt werden mit:
java -cp .;../classes;junit.jar junit.swingui.TestRunner meinpackage.MeineKlasseTest
Üblicher ist allerdings die Zusammenfassung mehrerer Testklassen zu einer Test-Suite, wie im Folgenden beschrieben ist.
Speichern Sie im Unterverzeichnis '<MeinJUnitDir>\tst' die folgende Test-Suite-Datei 'AllTests.java':
import junit.framework.Test;
import junit.framework.TestSuite;
public class AllTests extends TestSuite
{
public static Test suite()
{
TestSuite mySuite = new TestSuite( "Meine Test-Suite" );
mySuite.addTestSuite( meinpackage.MeineKlasseTest.class );
// ... weitere Testklassen hinzufügen
return mySuite;
}
}
Meistens werden sogar pro 'package' alle Testklassen zu einer TestSuite zusammengefasst, und dann werden in der gezeigten 'AllTests'-Klasse diese TestSuites nochmal zu einer TestSuite zusammengefasst.
Öffnen Sie ein Kommandozeilenfenster und geben Sie folgende Kommandos ein:
cd \Java\JUnit\tst
javac -d ../classes ../src/meinpackage/MeineKlasse.java
javac -d ../classes -classpath .;../classes;junit.jar AllTests.java
java -cp .;../classes;junit.jar junit.textui.TestRunner AllTests
java -cp .;../classes;junit.jar junit.swingui.TestRunner AllTests
Passen Sie den Pfad 'D:\Java\JUnit\tst' an Ihr '<MeinJUnitDir>' an.
'junit.textui.TestRunner' zeigt das Testergebnis im Kommandozeilenfenster, während
'junit.swingui.TestRunner' das Testergebnis im grafischen Swing-Dialog präsentiert
(Sie benötigen natürlich nur eine der beiden Versionen).
Klicken Sie im Swing-UI auf den Tabulatorreiter 'Test Hierarchy' und auf die Symbole vor den erscheinenden Zeilen, bis Sie die einzelnen Testmethoden sehen. Dann können Sie die Testmethoden auch einzeln anklicken und mit 'Run' ausführen.
Bauen Sie entweder in der zu testenden Klasse oder im Testcode Fehler ein, um Fehlermeldungen beobachten zu können.
Speichern Sie im Unterverzeichnis '<MeinJUnitDir>\tst' die folgende Datei 'Test.java':
public class Test
{
public static void main( String[] args )
{
(new junit.swingui.TestRunner()).start(
new String[] { "-noloading", "AllTests" } );
}
}
(Alternativ könnten Sie die main()-Methode auch einer bereits bestehenden Klasse, z.B. 'AllTests.java', hinzufügen.)
Öffnen Sie ein Kommandozeilenfenster und geben Sie folgende Kommandos ein:
cd \Java\JUnit\tst
javac -d ../classes ../src/meinpackage/MeineKlasse.java
javac -d ../classes -classpath .;../classes;junit.jar AllTests.java
javac -d ../classes -classpath .;../classes;junit.jar Test.java
java -cp .;../classes;junit.jar Test
Ersetzen Sie im Unterverzeichnis '<MeinJUnitDir>\tst' die Test-Suite-Datei 'AllTests.java' durch folgenden Inhalt:
import junit.framework.Test;
import junit.framework.TestSuite;
import junit.extensions.RepeatedTest;
public class AllTests extends TestSuite
{
public static Test suite()
{
TestSuite mySuite = new TestSuite( "Meine Test-Suite" );
mySuite.addTestSuite( meinpackage.MeineKlasseTest.class );
// ... weitere Testklassen hinzufügen
return new RepeatedTest( mySuite, 5 );
}
}
Starten Sie den Test wieder im Kommandozeilenfenster:
javac -d ../classes -classpath .;../classes;junit.jar AllTests.java
java -cp .;../classes;junit.jar junit.swingui.TestRunner AllTests
Die Tests werden jetzt fünfmal wiederholt. Das macht bei diesem einfachen Test natürlich keinen Sinn, aber es gibt viele Situationen, bei denen Fehler erst beim wiederholten Versuch auftreten, zum Beispiel bei konkurrierenden Threads, Message Queues oder bei Einbindung externer Komponenten über Netzwerk- oder Datenbankverbindungen.
Das folgende Beispiel sucht im CLASSPATH mit 1 beginnend fortlaufend durchnummerierte Properties-Dateien
mit den Namen 'Test1.properties', 'Test2.properties' usw. und zählt sie.
Entsprechend der Anzahl der gefundenen Properties-Dateien wird genau so oft der Test wiederholt
mit für jeden Test neue geladenen Testparametern aus der jeweils entsprechenden Properties-Datei.
Speichern Sie im Unterverzeichnis
'<MeinJUnitDir>\tst\meinpackage'
die folgende Testklassendatei 'MeineKlassePropTest.java':
package meinpackage;
import java.io.FileInputStream;
import java.util.Properties;
import junit.extensions.RepeatedTest;
import junit.framework.*;
public class MeineKlassePropTest extends TestCase
{
static int counterForUsedPropFile = 0; // counter for properties files
public static Test suite()
{
counterForUsedPropFile = 0;
// Count properties files (in classpath):
int i = 0;
Properties prop = new Properties();
while( true ) {
try {
prop.load( new FileInputStream( "Test" + ++i + ".properties" ) );
} catch( Exception ex ) {
break;
}
}
// Create RepeatedTest, for each properties file one test:
TestSuite mySuite = new TestSuite( "TestRepeater" );
mySuite.addTestSuite( MeineKlassePropTest.class );
// If no properties file found, we set '1' for getting an error
// message with 'fail()' in 'testReadParmsFromPropFile()':
return new RepeatedTest( mySuite, ( 0 < --i ) ? i : 1 );
}
public void testReadParmsFromPropFile() throws Exception
{
// Read parameters from properties file:
String propFileName = "Test" + ++counterForUsedPropFile + ".properties";
Properties prop = new Properties();
try {
prop.load( new FileInputStream( propFileName ) );
} catch( Exception ex ) {
fail( "Fehler: Properties-Datei '" + propFileName
+ "' fehlt (im CLASSPATH). " );
}
String job = prop.getProperty( "job" );
double val = Double.parseDouble( prop.getProperty( "val" ) );
double rslt = Double.parseDouble( prop.getProperty( "rslt" ) );
// Test 'MeineKlasse':
MeineKlasse meineKlasse1 = new MeineKlasse();
meineKlasse1.setJob( job );
if( "Quadrat".equalsIgnoreCase( job ) ||
"Wurzel".equalsIgnoreCase( job ) ) {
assertEquals( job + " von '" + val + "': ",
rslt, meineKlasse1.myMethod( val ), 0.001 );
} else {
fail( "'job' muss 'Quadrat' oder 'Wurzel' sein, ist aber '" + job + "'." );
}
}
}
Sie benötigen zusätzlich Properties-Dateien im CLASSPATH.
Speichern Sie im Unterverzeichnis
'<MeinJUnitDir>\tst'
die beiden folgenden Properties-Dateien.
'Test1.properties':
job=Wurzel val=64 rslt=8
'Test2.properties':
job=Quadrat val=64 rslt=4096
Ergänzen Sie weitere 'TestXX.properties'-Dateien.
Öffnen Sie ein Kommandozeilenfenster und geben Sie folgende Kommandos ein:
cd \Java\JUnit\tst
javac -d ../classes ../src/meinpackage/MeineKlasse.java
javac -d ../classes -classpath .;../classes;junit.jar meinpackage/MeineKlassePropTest.java
java -cp .;../classes;junit.jar junit.swingui.TestRunner meinpackage.MeineKlassePropTest
Achten Sie darauf, dass bei 'Runs' die korrekte Zahl der 'TestXX.properties'-Dateien angezeigt wird, also zum Beispiel 2/2.
Noch etwas komfortabler ist der Einsatz von JUnit, wenn Sie die gute Integration von JUnit in Eclipse nutzen. Dazu soll das letzte Projekt noch mal von Grund auf neu erstellt werden.
| Hansel und Gretel | Code Coverage, nicht ausgeführten Code ermitteln http://hansel.sourceforge.net, http://www.cs.uoregon.edu/research/perpetual/dasada/Software/Gretel |
| JUnitPerf | Performanztest, Zeitmessung, Timeout bei zu langer Ausführungszeit, Simulation mehrerer gleichzeitiger Benutzer http://clarkware.com/software/JUnitPerf.html |
| ServletUnit | Mock-Objekte (Attrappen) für das Servlet-API zum Test von Servlets außerhalb des Webcontainers,
auch für Struts geeignet http://sourceforge.net/projects/servletunit, http://httpunit.sourceforge.net/doc/servletunit-intro.html |
| MockEJB | Mock-Objekte (Attrappen) für das EJB-API zum Test von EJBs außerhalb des EJB-Containers http://www.mockejb.org |
| Cactus | Testen von Java-EE-Komponenten (Servlets, Servlet-Filter, JSP, JSP-Custom-Tags, EJB)
innerhalb eines Java-EE-Containers (z.B. Tomcat oder JBoss) http://jakarta.apache.org/cactus |
| DbUnit | Datenbankinhalt-Initialisierung, -Restaurierung, -Import, -Export (XML), Datenbanktabellenvergleiche http://dbunit.sourceforge.net |
| HttpUnit | Black-Box-Akzeptanztests von Webanwendungen, simuliert interaktive Website-Besuchssequenzen, prüft SSL, HTTP-Header, Frames, Tabellen, Formulare, Links, JavaScript, Cookies techdocs/java-httpunit.htm, http://httpunit.sourceforge.net, http://webtest.canoo.com |
| Jemmy | Testen von Swing-GUIs http://jemmy.netbeans.org |
| JUnitReport | Erstellung übersichtlicher Ergebnisdarstellungen in HTML http://ant.apache.org/manual/OptionalTasks/junitreport.html |