Unit-Tests
Ziel von Unit-Tests
Um fehlerarme Software zu entwickeln, muss diese ausgiebig getestet werden. Testen ist deshalb eine sehr wichtige Arbeit, die allerdings in der Praxis zu oft vernachlässigt wird, da Testen zusätzlichen Aufwand bedeutet, der meist auch nicht besonders viel Spaß macht.
Hier kommen automatisierte Unit-Tests bzw. Modultests ins Spiel: Ziel von Unit-Tests ist es einzelne Teile (Module) von Software zu testen. Im Idealfall erfolgt dies automatisch, damit bei einer Änderung der Software nicht alle durchgeführten Tests per Hand wiederholt werden müssen. In Java gibt es dafür das JUnit-Framework, das einem die Arbeit erleichtert.
Objektzustände in BlueJ speichern
In BlueJ kann man JUnit "missbrauchen", um sich schnell und einfach den Zustand der Objekte der Objektleiste zu speichern. Wie man das macht, ist im Anhang - BlueJ optimal nutzen beschrieben. Das kann sehr praktisch sein, hat aber mit Tests nur bedingt zu tun, weshalb hier nicht weiter darauf eingegangen wird.
Ausgangsbeispiel
Als Basis für eine kleine Beispielsoftware, die getestet werden soll,
soll eine einfache Mensch
-Klasse dienen (kein UML sondern Java-spezifisch):
Ein Mensch
hat ein alter
, das man setzen und abfragen kann.
Beim Setzen des Alters sollen nur sinnvolle Werte zulässig sein.
Andernfalls wird das Alter nicht verändert.
Außerdem kann man abfragen, ob ein Mensch volljährig ist.
Ein Mensch
A kann heiraten, und kennt
anschließend seinen partner
B.
B soll dann automatisch auch mit A verheiratet sein.
Testgetriebene Entwicklung
Ein Grund, warum Tests oft vernachlässigt werden, ist, weil die Tests im Anschluss an die Implementierung erfolgen müssen. Oft wird die Zeit dann knapp und Tests erfolgen nur oberflächlich, da man ja gewissenhaft programmiert hat, und schon hoffentlich alles funktionieren wird.
Wenn man schon bei der Implementierung Testfälle anlegt, können diese mit Hilfe von JUnit automatisch ausgeführt und ausgewertet werden. Testen beschränkt sich dann, vorausgesetzt alle Tests sind erfolgreich, auf einen Mausklick. Ein weiterer Vorteil dieser Vorgehensweise ist, dass man sich schon vor der Implementierung Gedanken zu möglichen Fällen, die eintreten können, macht.
Um Testfälle anlegen zu können, muss man zuerst das Grundgerüst für den zu testenden Code anlegen.
Damit man den Code compilieren kann, müssen Methoden mit Rückgabewert
Dummy-Werte zurückgeben. Diese müssen zum Rückgabetyp der Methode passen.
Bei Klassen als Rückgabetyp kann man z.B. einfach null
zurück geben.
Das Grundgerüst kann somit folgende Gestalt haben:
class Mensch
{
int alter;
Mensch partner;
int getAlter() {
return 0;
}
void setAlter(int a) {
}
boolean istVolljaehrig() {
return true;
}
void heiraten(Mensch p) {
}
Mensch getPartner() {
return null;
}
}
JUnit-Tests in BlueJ interaktiv erstellen
Hat man das Grundgerüst geschrieben und eine syntaktisch korrekte Klasse als Basis, lassen sich schon alle später durchzuführenden Tests formulieren. BlueJ bietet einem besonderen Komfort, da man hier die Tests interaktiv erstellen kann, ohne eine Zeile Code schreiben zu müssen.
Der folgende Schritt ist für BlueJ ab Version 4 nicht mehr notwendig: Zuerst müssen in BlueJ die Testwerkzeuge aktiviert werden. Unter Windows aktivierst Du dazu die Option Teamwerkzeuge anzeigen unter Werkzeuge→ Einstellungen→ Interface. Unter OS X aktivierst Du die Option unter BlueJ→ Einstellungen→ Interface.
Für Klassen, die getestet werden sollen, legt man durch Rechtsklick auf die Klassen entsprechende Testklassen an:
Um klassenübergreifende Tests zu erstellen, kann man alternativ eine neue Klasse vom Typ Unit-Test erstellen:
Die Funktionalität unterscheidet sich nicht, sondern nur die Darstellung in BlueJ:
Anschließend kann man beliebig viele Tests erstellen. Alle interaktiv erzeugten Objekte und Methodenaufrufe werden dabei gespeichert. Bei Methoden mit Rückgabewerten kann man dann die erwarteten Werte eingeben:
Dies funktioniert auch, wenn Referenzen auf Objekte zurückgegeben werden sollen:
Anschließend könne alle Tests durch Klick auf Tests starten automatisch ausgeführt werden. Dabei wird dann überprüft, ob die Rückgabewerte der Methoden mit den erwarteten Rückgaben übereinstimmen. Da wir bisher nur Dummywerte zurückgeben, können die Tests hier natürlich noch nicht erfolgreich sein:
Aufgabe 1 - Testfälle erstellen
Probiere das alles wie oben dargestellt aus. Ergänze mindestens einen Test, der überprüft, ob die
setAlter
-Methode funktioniert. Diese soll bei sinnvollen Werten das Attribut
alter
überschreiben. Ein negatives Alter oder ein Alter über 130
sollen ignoriert werden; das Alter bleibt dann unverändert.
Ergänze die Implementierung, so dass die Tests erfolgreich ablaufen.
JUnit-Tests als Programmcode
BlueJ erstellt aus den interaktiv erstellten Tests Quellcode.
In anderen Entwicklungsumgebungen muss der Code für die Tests selbst geschrieben werden.
Aber auch innerhalb von BlueJ kann es komfortabler sein, Testcode zu erweitern, anstatt neue Testfälle
"zusammenzuklicken". Die im obigen Video interaktiv erstellte Testmethode testHeiraten
wird von BlueJ übersetzt in folgenden Code:
@Test
public void testHeiraten()
{
Mensch mensch1 = new Mensch();
Mensch mensch2 = new Mensch();
mensch1.heiraten(mensch2);
assertEquals(mensch2, mensch1.getPartner());
assertEquals(mensch1, mensch2.getPartner());
}
Bei der Angabe @Test
handelt es sich um eine sogenannte Annotation.
Wir gehen hier nicht weiter auf Annotations ein, sondern halten nur fest, dass
diese vor einem Test angegeben sein muss.
Innerhalb der Methode wurden dann genau die Aktionen, die wir per Maus durchgeführt haben, codiert.
Um erwartete und reale Rückgabewerte von Methoden zu vergleichen, nutzt man die Methode
assertEquals
, die im JUnit-Framework definiert ist.
Dieses Wissen über Testmethoden reicht aus, um Testmethoden leicht abzuändern oder zu ergänzen.
Auf diese Art hat man z.B. auch Zugriff auf Attribute (sofern diese nicht als private
definiert sind),
oder man kann komplexere Ausdrücke formulieren.
Man könnte obige Methode dann z.B. folgendermaßen formulieren:
@Test
public void testHeiratenManuell()
{
Mensch a = new Mensch();
Mensch b = new Mensch();
a.heiraten(b);
assertEquals(b, a.getPartner());
assertEquals(a, a.getPartner().getPartner());
}
Ein anderer Anwendungsfall könnte gegeben sein, wenn man auf Attribute zugreifen möchte (und darf):
@Test
public void testSetAlter()
{
Mensch m = new Mensch();
m.setAlter(10);
m.setAlter(200);
m.setAlter(-5);
assertEquals(10, m.alter); // Hier wird auf Attribut zugegriffen
}
Weitere Methoden
Neben der Methode assertEquals(...)
gibt es noch einige andere Methoden,
um die korrekte Funktionsweise des Programms zu überprüfen:
-
assertNull(...)
/assertNotNull(...)
: Überprüft, ob die als Parameter übergebene Referenz (un)gleichnull
ist. -
assertSame(...)
/assertNotSame(..., ...)
: Überprüft, ob die übergebenen Referenzen auf das selbe (ein anderes) Objekt zeigen. -
assertEquals(..., ...)
/assertNotEquals(..., ...)
: Überprüft, ob die übergebenen Parameter den gleichen (einen anderen) Wert besitzen. Bei Objekten wird die vonObject
geerbte und eventuell überschriebene Methodeequals(...)
zur Überprüfung herangezogen.
@Test
public void beispieltests()
{
String s1 = new String("Hallo");
String s2 = new String("Hallo");
assertNotNull(s1); // erfüllt, da s1 nicht null
assertNotSame(s1, s2); // erfüllt, da s1 und s2 unterschiedliche Objekte sind
assertEquals(s1, s2); // erfüllt, da s1 und s2 "Hallo" als Wert haben
}
Aufgabe 2 - Manueller Code
Erstelle weitere Tests manuell oder erweitere schon vorhandene Tests.