JUnit
🚧 | Diese Seite befindet sich in Bearbeitung | 🚧 |
Automatische Komponententests kommen heutzutage bei sämtlichen Programmierprojekten vor. Spezielle Test-Frameworks vereinfachen das Erstellen und Ausführen dieser Tests. Die Grundkonzepte stammen von SUnit, einem Test-Framework von Kent Beck für die Programmiersprache Smalltalk. Mittlerweile existieren für jede Programmiersprache ähnliche Frameworks (CppUnit für C++, NUnit für C#, PHPUnit für PHP, etc...) oder sind gar Bestandteil der Standardbibliothek (Python, Ruby, etc...). Die bekannteste Test-Suite für Java ist dabei JUnit. Im folgenden gibt es eine kleine Einführung in Komponententests und JUnit. Dieses Thema wird erneut und in größerem Teil in der späteren Vorlesung Softwaretechnik (SWT) aufgegriffen.
Wie komme ich an JUnit?
Eclipse
JUnit kommt mittlerweile standardmäßig mit Eclipse. JUnit-Testklassen können über File -> New -> JUnit Test Case erstellt werden. Hier kann JUnit allerdings auch manuell heruntergeladen werden.
IntelliJ
Hier ist die Erstellung einer Test-Klasse zwar auch einfach aber muss manuell geschehen. Hierzu muss zunächst eine neue, leere Klasse erstellt werden. Diese sollte als Namen einen Namen für die Gruppe an Tests haben, welche die Klasse enthält und mit Tests enden. Auch muss dann die Klasse org.junit.jupiter.api.Test importiert werden. Wie genau einzelne Tests erstellt werden, folgt in einem späteren Abschnitt.
Wozu eigentlich Komponententests?
Bevor wir uns mit den Tests selbst beschäftigen wollen wir erst klären wieso diese eigentlich sinnvoll sind. Sie helfen unter anderem bei der Überprüfung der funktionalen Richtigkeit der implementierten Methoden.
Soll zum Beispiel eine Klasse um eine equals()-Methode (und der damit einhergehenden hashCode()-Methode) erweitert werden, schreibt die Java-API einige Bedingungen für diese vor:
- It is reflexive: for any non-null reference value x, x.equals(x) should return true
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true, if and only if y.equals(x) returns true
- It is transitive: for any non-null reference values x, y and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified
- For any non-null reference value x, x.equals(null) should return false
Mit JUnit-Tests können wir nun die oben beschriebenen Bedingungen mit Beispielobjekten überprüfen.
Wie werden Komponententests geschrieben?
Im folgenden soll das Testen mit JUnit an einem Beispiel gezeigt werden.
Die ziemlich rudimentäre Student-Klasse soll um equals()- und hashCode()-Methoden erweitert werden:
package kit;
public class Student {
private String name;
private String surName;
private int matriculationNumber;
public Student(String name, String surName, int matriculationNumber) {
this.name = name;
this.surName = surName;
this.matriculationNumber = matriculationNumber;
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return super.hashCode();
}
}
Um die Test-Klasse zu erstellen importieren wir die zu testende Klasse (also kit.Student). Die JUnit-Imports sorgen für die Verfügbarkeit der @Test-Annotationen (org.junit.Test) und der assert-Methoden (org.junit.Assert). @Test-Methoden sind Testfällt, die von JUnit ausgeführt werden und Methoden des Testziels testen. Die Tests werden dann als fehlgeschlafen markiert, wenn eine der enthaltenen assert-Methoden eine Abweichung von der Spezifikation findet:
package kittest;
import kit.student;
import org.junit.Assert;
import org.junit.Test;
public class StudentTests {
@Test
public void equalsReflexiveTest() {
Student mustermann = new Student("Mustermann", "Max", 1234567);
Assert.assertTrue(mustermann.equals(mustermann));
}
@Test
public void equalsSymmetricTest() {
Student mustermann = new Student("Mustermann", "Max", 1234567);
Student musterfrau = new Student("Musterfrau", "Marie", 1234567);
Assert.assertTrue(musterfrau.equals(mustermann));
Assert.assertTrue(mustermann.equals(musterfrau));
}
@Test
public void equalsNullTest() {
Student mustermann = new Student("Mustermann", "Max", 1234567);
Assert.assertFalse(mustermann.equals(null));
}
@Test
public void equalsUnequalTest() {
Student mustermann = new Student("Mustermann", "Max", 1234567);
Student musterfrau = new Student("Musterfrau", "Marie", 7654321);
Assert.assertFalse(mustermann.equals(musterfrau));
Assert.assertFalse(musterfrau.equals(mustermann));
}
@Test
public void hashCodeTest() {
Student mustermann = new Student("Mustermann", "Max", 1234567);
Student musterfrau = new Student("Musterfrau", "Marie", 1234567);
Assert.assertEquals(musterfrau.hashCode(), mustermann.hashCode());
}
}
Das Testen unserer bisherigen Implementierung mit der erstellten Testklasse liefert folgendes Ergebnis:
Zwei der fünf geschriebenen Tests sind fehlgeschlagen. Daran sehen wir, dass unsere Implementierung noch nicht ganz richtig ist. Es werden nur Referenzen überprüft (was der Standardimplementierung der equals()-Methode in der Klasse Object entspricht).
Wir wollen diese Methoden nun so implementieren, dass zwei Studenten gleich sind, genau dann, wenn ihre Matrikelnummern gleich sind:
package kit;
public class Student {
private String name;
private String surName;
private int matriculationNumber;
public Student(String name, String surName, int matriculationNumber) {
this.name = name;
this.surName = surName;
this.matriculationNumber = matriculationNumber;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (this.getClass().equals(obj.getClass())) {
return false;
}
Student objStudent = (Student) obj; // This cast is safe since we excluded all sources of error beforehand
return this.matriculationNumber = objStudent.matriculationNumber;
}
}
Alle fünf Tests waren mit dieser Implementierung erfolgreich. Der Code erfüllt also das von uns gewünschte Verhalten in den von uns getesteten Szenarien.
Welche @-Annotationen gibt es in JUnit?
Annotation | Beschreibung |
---|---|
@Test | kennzeichnet Methoden als ausführbare Testfälle |
@BeforeClass | kennzeichnet SetUp-Methoden, die einmal pro Testklasse vor allen Testfällen ausgeführt werden |
@Before | kennzeichnet SetUp-Methoden, die vor jedem Testfall ausgeführt werden |
@AfterClass | kennzeichnet TearDown-Methoden, die einmal pro Testklasse nach allen Testfällen ausgeführt werden |
@After | kennzeichnet TearDown-Methoden, die einmal nach jedem Testfall ausgeführt werden |
@Ignore | kennzeichnet nicht auszuführende Testfälle |
Hier gibt es noch mehr zum Nachlesen.
Welche assert-Methoden gibt es in JUnit?
Methode | Verwendung |
---|---|
assertEquals(Object expected, Object actual) | Schlägt fehl, wenn der Aufruf der equals()-Methode von expected mit dem Parameter actual den Wert false zurückgibt |
assertSame(Object expected, Object actual) | Schlägt fehl, wenn die Referenzen auf unterschiedliche Objekte zeigen |
assertEquals(double expected, double actual, double delta) | Schlägt fehl, wenn sich expected und actual um mehr als delta unterscheiden |
assertTrue(boolean actual)/assertFalse(boolean actual) | Schlägt fehl, wenn der Ausdruck false bzw. true ist |
assertNull(Object actual)/assertNotNull(Object actual) | Schlägt fehl, wenn die Referenz nicht null bzw. null ist |
assertArrayEquals(Object[] expected, Object[] actual) | Schlägt fehl, wenn die Arrays unterschiedliche Werte besitzen |
Hier gibt es noch mehr Assert-Methoden zum Nachlesen.
Wie ist die Reihenfolge der @-Annotationen beim Ausführen der Testklassen?
- @BeforeClass
- @Before
- @Test
- @After
- @Before
- @Test
- @After
- @AfterClass
Die Reihenfolge der @Test-Methoden müssen nicht der Reihenfolge der Methoden im Testklassen-Quelltext entsprechen. Testmethode 2 kann also vor Testmethode 1 ausgeführt werden.
Wie können Exceptions getestet werden?
Angenommen wir wollen eine Methode testen, die einen Referenztyp als Parameter fordert. Wenn null als Argument beim Methodenaufruf übergeben wird, soll eine NullArgumentException geworfen werden. Auch das können wir mit JUnit testen:
@Test(expected = IllegalArgumentException.class)
public void illegalArgumentTest() {
ExampleObject obj = new ExampleObject();
obj.paramMustNotBeNull(null);
}
Hier ist der Test erfolgreich, wenn die Methode paramMustBeNull() mit gegebenem Parameter null eine IllegalArgumentException wirft.
Wie werden Komponententests sicher gegen Endlosschleifen programmiert?
Um das Terminieren der Tests auch bei Methoden mit Endlosschleifen zu garantieren, können wir Tests mit Timeouts versehen:
@Test(timeout = 10000)
public void timeoutTest() {
while(true) {}
}
Die Zahl gibt die Anzahl der Millisekunden an, nach denen der Testlauf abgebrochen werden soll. In dem Fall wird dieser Test als fehlgeschlagen markiert.
Was ist beim Schreiben von Komponententests zu beachten?
Testmethoden sollten so geschrieben sein, dass sie nicht von anderem Code abhängen, als dem, der in der eigenen Methode steht. Ausnahmen davon sind die Testumgebung, die von @Before und @beforeClass und deren Gegenstücken verwaltet werden.
Einerseits liegt das daran, dass die Reihenfolge der Ausführung von JUnit nicht derjenigen im Quellcode entspricht, andererseits sollten die Komponententests alleine lauffähig sein. Wenn ein Fehler in einer Methode auftrat und man eine mögliche lösung implementiert hat, dann ist es angenehm, wenn einfach nur der fehlgeschlagene Test einzeln ausgeführt werden kann, ohne gleich alle Tests erneut starten zu müssen. Dies bietet sich vor allem in großen Projekten an.
Wie werden kompliziertere Testumgebungen erstellt?
Manchmal kommt es vor, dass das Anlegen eines einzelnen Objekts, anders wie beim obigen Student-Objekt, nicht ausreicht, um die gewünschten Methoden zu testen. Möglicherweise ist der Aufbau der Testumgebung sehr kompliziert und lang. Darüber hinaus ja sogar auch noch fast identisch für viele Tests. Für solche Fälle gibt es, analog zur @Test-Annotation, die zuvor schon kurz angemerkte @Before-Annotation. Methoden, die derart annotiert sind, werden vor jedem Testfall ausgeführt. Das Gegenstück dazu ist die @After-Annotation. Diese wird nach jedem Testfall ausgeführt.
Kommt es vor, dass eine Methode einmal am Anfang des gesamten Testablaufs ausgeführt werden soll, wird die @BeforeClass-Annotation und ihr Gegenstück, die @AfterClass-Annotation verwendet.
In diesem Artikel wurde JUnit wie bereits am Anfang erwähnt nur angebrochen. Hier kann allerdings alles nochmal mehr im Detail nachgelesen werden. Testen