Subsections of Testen mit JUnit und Mockito
Einführung Softwaretest
Fehler schleichen sich durch Zeitdruck und hohe Komplexität schnell in ein Softwareprodukt ein. Die Folgen können von "ärgerlich" über "teuer" bis hin zu (potentiell) "tödlich" reichen. Richtiges Testen ist also ein wichtiger Aspekt bei der Softwareentwicklung!
JUnit ist ein Java-Framework, mit dem Unit-Tests (aber auch andere Teststufen) implementiert werden
können. In JUnit 4 und 5 zeichnet man eine Testmethode mit Hilfe der Annotation @Test
an der
entsprechenden Methode aus. Dadurch kann man Produktiv- und Test-Code prinzipiell mischen; Best
Practice ist aber das Anlegen eines weiteren Ordners test/
und das Spiegeln der Package-Strukturen.
Für die zu testende Klasse wird eine korrespondierende Testklasse mit dem Suffix "Test" (Konvention)
angelegt und dort die Testmethoden implementiert. Der IDE muss der neue test/
-Ordner noch als
Ordner für Sourcen bzw. Tests bekannt gemacht werden. In den Testmethoden baut man den Test auf,
führt schließlich den Testschritt durch (beispielsweise konkreter Aufruf der zu testenden Methode)
und prüft anschließend mit einem assert*()
, ob das erzielte Ergebnis dem erwarteten Ergebnis
entspricht. Ist alles OK, ist der Test "grün", sonst "rot".
Da ein fehlschlagendes assert*()
den Test abbricht, werden eventuell danach folgende Prüfungen
nicht mehr durchgeführt und damit ggf. weitere Fehler maskiert. Deshalb ist es gute Praxis, in
einer Testmethode nur einen Testfall zu implementieren und i.d.R. auch nur ein (oder wenige) Aufrufe
von assert*()
pro Testmethode zu haben.
- (K2) Ursachen von Softwarefehlern
- (K3) Aufbauen von Tests mit JUnit 4 und 5 unter Nutzung der Annotation
@Test
Software-Fehler und ihre Folgen
(Einige) Ursachen für Fehler
- Zeit- und Kostendruck
- Mangelhafte Anforderungsanalyse
- Hohe Komplexität
- Mangelhafte Kommunikation
- Keine/schlechte Teststrategie
- Mangelhafte Beherrschung der Technologie
- ...
Irgendjemand muss mit Deinen Bugs leben!
Leider gibt es im Allgemeinen keinen Weg zu zeigen, dass eine Software korrekt ist. Man kann (neben formalen Beweisansätzen) eine Software nur unter möglichst vielen Bedingungen ausprobieren, um zu schauen, wie sie sich verhält, und um die dabei zu Tage tretenden Bugs zu fixen.
Mal abgesehen von der verbesserten User-Experience führt weniger fehlerbehaftete Software auch dazu, dass man seltener mitten in der Nacht geweckt wird, weil irgendwo wieder ein Server gecrasht ist ... Weniger fehlerbehaftete Software ist auch leichter zu ändern und zu pflegen! In realen Projekten macht Maintenance den größten Teil an der Softwareentwicklung aus ... Während Ihre Praktikumsprojekte vermutlich nach der Abgabe nie wieder angeschaut werden, können echte Projekte viele Jahre bis Jahrzehnte leben! D.h. irgendwer muss sich dann mit Ihren Bugs herumärgern - vermutlich sogar Sie selbst ;)
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live. Code for readability.
Dieses Zitat taucht immer mal wieder auf, beispielsweise auf der OSCON 2014 ... Es scheint aber tatsächlich, dass John F. Woods die ursprüngliche Quelle war (vgl. Stackoverflow: 876089).
Da wir nur wenig Zeit haben und zudem vergesslich sind und obendrein die Komplexität eines Projekts mit der Anzahl der Code-Zeilen i.d.R. nicht-linear ansteigt, müssen wir das Testen automatisieren. Und hier kommt JUnit ins Spiel :)
Was wann testen? Wichtigste Teststufen
-
Modultest
- Testen einer Klasse und ihrer Methoden
- Test auf gewünschtes Verhalten (Parameter, Schleifen, ...)
-
Integrationstest
- Test des korrekten Zusammenspiels mehrerer Komponenten
- Konzentration auf Schnittstellentests
-
Systemtest
- Test des kompletten Systems unter produktiven Bedingungen
- Orientiert sich an den aufgestellten Use Cases
- Funktionale und nichtfunktionale Anforderungen testen
=> Verweis auf Wahlfach "Softwarequalität"
JUnit: Test-Framework für Java
JUnit --- Open Source Java Test-Framework zur Erstellung und Durchführung wiederholbarer Tests
-
JUnit 3
- Tests müssen in eigenen Testklassen stehen
- Testklassen müssen von Klasse
TestCase
erben - Testmethoden müssen mit dem Präfix "
test
" beginnen
-
JUnit 4
- Annotation
@Test
für Testmethoden - Kein Zwang zu spezialisierten Testklassen (insbesondere kein Zwang mehr zur Ableitung von
TestCase
) - Freie Namenswahl für Testmethoden (benötigen nicht mehr Präfix "
test
")
Damit können prinzipiell auch direkt im Source-Code Methoden als JUnit-Testmethoden ausgezeichnet werden ... (das empfiehlt sich in der Regel aber nicht)
- Annotation
-
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- Erweiterung um mächtigere Annotationen
- Aufteilung in spezialisierte Teilprojekte
Das Teilprojekt "JUnit Platform" ist die Grundlage für das JUnit-Framework. Es bietet u.a. einen Console-Launcher, um Testsuiten manuell in der Konsole zu starten oder über Builder wie Ant oder Gradle.
Das Teilprojekt "JUnit Jupiter" ist das neue Programmiermodell zum Schreiben von Tests in JUnit 5. Es beinhaltet eine TestEngine zum Ausführen der in Jupiter geschriebenen Tests.
Das Teilprojekt "JUnit Vintage" beinhaltet eine TestEngine zum Ausführen von Tests, die in JUnit 3 oder JUnit 4 geschrieben sind.
Anmerkung: Wie der Name schon sagt, ist das Framework für Modultests ("Unit-Tests") gedacht. Man kann damit aber auch auf anderen Teststufen arbeiten!
Anmerkung: Im Folgenden besprechen wir JUnit am Beispiel JUnit 4, da diese Version des Frameworks besonders stark verbreitet ist und JUnit 5 (trotz offiziellem Release) immer noch stellenweise unfertig wirkt. Auf Unterschiede zu JUnit 5 wird an geeigneter Stelle hingewiesen (abgesehen von Import-Statements). Mit JUnit 3 sollte nicht mehr aktiv gearbeitet werden, d.h. insbesondere keine neuen Tests mehr erstellt werden, da diese Version nicht mehr weiterentwickelt wird.
Anlegen und Organisation der Tests mit JUnit
-
Anlegen neuer Tests: Klasse auswählen, Kontextmenü
New > JUnit Test Case
-
Best Practice: Spiegeln der Paket-Hierarchie
- Toplevel-Ordner
test
(stattsrc
) - Package-Strukturen spiegeln
- Testklassen mit Suffix "
Test
"
- Toplevel-Ordner
Vorteile dieses Vorgehens:
- Die Testklassen sind aus Java-Sicht im selben Package wie die Source-Klassen, d.h. Zugriff auf Package-sichtbare Methoden etc. ist gewährleistet
- Durch die Spiegelung der Packages in einem separaten Testordner erhält man eine gute getrennte Übersicht über jeweils die Tests und die Sourcen
- Die Wiederverwendung des Klassennamens mit dem Anhang "Test" erlaubt die schnelle Erkennung, welche Tests hier vorliegen
In der Paketansicht liegen dann die Source- und die Testklassen immer direkt hintereinander (da sie im selben Paket sind und mit dem selben Namen anfangen) => besserer Überblick!
Anmerkung: Die (richtige) JUnit-Bibliothek muss im Classpath liegen!
Eclipse bringt für JUnit 4 und JUnit 5 die nötigen Jar-Dateien mit und fragt beim erstmaligen Anlegen einer neuen Testklasse, ob die für die ausgewählte Version passenden JUnit-Jars zum Build-Path hinzugefügt werden sollen.
IntelliJ bringt ebenfalls eine JUnit 4 Bibliothek mit, die zum Projekt als Abhängigkeit hinzugefügt werden muss. Für JUnit 5 bietet IntelliJ an, die Jar-Dateien herunterzuladen und in einem passenden Ordner abzulegen.
Alternativ lädt man die Bibliotheken entsprechend der Anleitung unter junit.org herunter und bindet sie in das Projekt ein.
JUnit 4+5: Definition von Tests
Annotation @Test
vor Testmethode schreiben
import org.junit.Test;
import static org.junit.Assert.*;
public class FactoryBeispielTest4 {
@Test
public void testGetTicket() {
fail("not implemented");
}
}
Für JUnit 5 muss statt org.junit.Test
entsprechend org.junit.jupiter.api.Test
importiert werden.
Während in JUnit 4 die Testmethoden mit der Sichtbarkeit public
versehen sein müssen und keine
Parameter haben (dürfen), spielt die Sichtbarkeit in JUnit 5 keine Rolle (und die Testmethoden dürfen Parameter
aufweisen => vgl. Abschnitt "Dependency Injection for Constructors and Methods" in der JUnit-Doku).
JUnit 4: Ergebnis prüfen
Klasse org.junit.Assert
enthält diverse statische Methoden zum Prüfen:
// Argument muss true bzw. false sein
void assertTrue(boolean);
void assertFalse(boolean);
// Gleichheit im Sinne von equals()
void assertEquals(Object, Object);
// Test sofort fehlschlagen lassen
void fail();
...
Für JUnit 5 finden sich die Assert-Methoden im Package org.junit.jupiter.api.Assertions
.
Anmerkung zum statischen Import
Bei normalem Import der Klasse Assert
muss man jeweils den voll qualifizierten Namen
einer statischen Methode nutzen: Assert.fail()
.
Alternative statischer Import: import static org.junit.Assert.fail;
=> Statische Member der importierten Klasse (oder Interface)
werden über ihre unqualifizierten Namen zugreifbar.
Achtung: Namenskollisionen möglich!
// nur bestimmtes Member importieren
import static packageName.className.staticMemberName;
// alle statischen Member importieren
import static packageName.className.*;
-
Beispiel normaler Import:
import org.junit.Assert; Assert.fail("message");
-
Beispiel statischer Import:
import static org.junit.Assert.fail; fail("message");
Mögliche Testausgänge bei JUnit
-
Error: Fehler im Programm (Test)
- Unbehandelte Exception
- Abbruch (Timeout)
-
Failure: Testausgang negativ
- Assert fehlgeschlagen
Assert.fail()
aufgerufen
-
OK
Anmerkungen zu Assert
- Pro Testmethode möglichst nur ein Assert verwenden!
- Anderenfalls: Schlägt ein Assert fehl, wird der Rest nicht mehr überprüft ...
Wrap-Up
-
Testen ist genauso wichtig wie Coden
-
Richtiges Testen spart Geld, Zeit, ...
-
Tests auf verschiedenen Abstraktionsstufen
-
JUnit als Framework für (Unit-) Tests; hier JUnit 4 (mit Ausblick auf JUnit 5)
- Testmethoden mit Annotation
@Test
- Testergebnis mit
assert*
prüfen
- Testmethoden mit Annotation
- [fernunihagenJunit] Einführung in JUnit
Thies, A. und Noelke, C. und Ungerc, Fernuniversität in Hagen. - [junit4] JUnit 5
The JUnit Team, 2022. - [Kleuker2019] Qualitätssicherung durch Softwaretests
Kleuker, S., Springer Vieweg, 2019. ISBN 978-3-658-24886-4. DOI 10.1007/978-3-658-24886-4. - [Osherove2014] The Art of Unit Testing
Osherove, R., Manning, 2014. ISBN 978-1-6172-9089-3. - [Spillner2012] Basiswissen Softwaretest
Spillner, A. und Linz, T., dpunkt, 2012. ISBN 978-3-86490-024-2. - [vogellaJUnit] JUnit 5 tutorial - Learn how to write unit tests
vogella GmbH, 2021.
Testen mit JUnit (JUnit-Basics)
In JUnit 4 und 5 werden Testmethoden mit Hilfe der Annotation @Test
ausgezeichnet. Über die
verschiedenen assert*()
-Methoden kann das Testergebnis mit dem erwarteten Ergebnis verglichen
werden und entsprechend ist der Test "grün" oder "rot". Mit den verschiedenen assume*()
-Methoden
kann dagegen geprüft werden, ob eventuelle Vorbedingungen für das Ausführen eines Testfalls
erfüllt sind - anderenfalls wird der Testfall dann übersprungen.
Mit Hilfe von @Before
und @After
können Methoden gekennzeichnet werden, die jeweils vor jeder
Testmethode und nach jeder Testmethode aufgerufen werden. Damit kann man seine Testumgebung auf-
und auch wieder abbauen (JUnit 4).
Erwartete Exceptions lassen sich in JUnit 4 mit einem Parameter expected
in der Annotation @Test
automatisch prüfen: @Test(expected=package.Exception.class)
. In JUnit 4 besteht die Möglichkeit,
Testklassen zu Testsuiten zusammenzufassen und gemeinsam laufen zu lassen.
- (K3) Steuern von Tests (ignorieren, zeitliche Begrenzung)
- (K3) Prüfung von Exceptions
- (K3) Aufbau von Testsuiten mit JUnit
JUnit: Ergebnis prüfen
Klasse org.junit.Assert
enthält diverse statische Methoden zum Prüfen:
// Argument muss true bzw. false sein
void assertTrue(boolean);
void assertFalse(boolean);
// Gleichheit im Sinne von equals()
void assertEquals(Object, Object);
// Test sofort fehlschlagen lassen
void fail();
...
To "assert" or to "assume"?
-
Mit
assert*
werden Testergebnisse geprüft- Test wird ausgeführt
- Ergebnis: OK, Failure, Error
-
Mit
assume*
werden Annahmen über den Zustand geprüft- Test wird abgebrochen, wenn Annahme nicht erfüllt
- Prüfen von Vorbedingungen: Ist der Test hier ausführbar/anwendbar?
Setup und Teardown: Testübergreifende Konfiguration
private Studi x;
@Before
public void setUp() { x = new Studi(); }
@Test
public void testToString() {
// Studi x = new Studi();
assertEquals(x.toString(), "Heinz (15cps)");
}
@Before
- wird vor jeder Testmethode aufgerufen
@BeforeClass
- wird einmalig vor allen Tests aufgerufen (
static
!) @After
- wird nach jeder Testmethode aufgerufen
@AfterClass
- wird einmalig nach allen Tests aufgerufen (
static
!)
In JUnit 5 wurden die Namen dieser Annotationen leicht geändert:
JUnit 4 | JUnit 5 |
---|---|
@Before |
@BeforeEach |
@After |
@AfterEach |
@BeforeClass |
@BeforeAll |
@AfterClass |
@AfterAll |
Beispiel für den Einsatz von @Before
Annahme: alle/viele Testmethoden brauchen neues Objekt x
vom Typ Studi
private Studi x;
@Before
public void setUp() {
x = new Studi("Heinz", 15);
}
@Test
public void testToString() {
// Studi x = new Studi("Heinz", 15);
assertEquals(x.toString(), "Name: Heinz, credits: 15");
}
@Test
public void testGetName() {
// Studi x = new Studi("Heinz", 15);
assertEquals(x.getName(), "Heinz");
}
Ignorieren von Tests
- Hinzufügen der Annotation
@Ignore
- Alternativ mit Kommentar:
@Ignore("Erst im nächsten Release")
@Ignore("Warum ignoriert")
@Test
public void testBsp() {
Bsp x = new Bsp();
assertTrue(x.isTrue());
}
In JUnit 5 wird statt der Annotation @Ignore
die Annotation @Disabled
mit
der selben Bedeutung verwendet. Auch hier lässt sich als Parameter ein String
mit dem Grund für das Ignorieren des Tests hinterlegen.
Vermeidung von Endlosschleifen: Timeout
- Testfälle werden nacheinander ausgeführt
- Test mit Endlosschleife würde restliche Tests blockieren
- Erweitern der
@Test
-Annotation mit Parameter "timeout
": =>@Test(timeout=2000)
(Zeitangabe in Millisekunden)
In JUnit 5 hat die Annotation @Test
keinen timeout
-Parameter mehr.
Als Alternative bietet sich der Einsatz von org.junit.jupiter.api.Assertions.assertTimeout
an. Dabei benötigt man allerdings Lambda-Ausdrücke (Verweis auf spätere VL):
@Test
void testTestDauerlaeufer() {
assertTimeout(ofMillis(2000), () -> {
while (true) { ; }
});
}
(Beispiel von oben mit Hilfe von JUnit 5 formuliert)
Test von Exceptions: Expected
Traditionelles Testen von Exceptions mit try
und catch
:
@Test
public void testExceptTradit() {
try {
int i = 0 / 0;
fail("keine ArithmeticException ausgeloest");
} catch (ArithmeticException aex) {
assertNotNull(aex.getMessage());
} catch (Exception e) {
fail("falsche Exception geworfen");
}
}
Der expected
-Parameter für die @Test
-Annotation in JUnit 4 macht
dies deutlich einfacher: @Test(expected = MyException.class)
=> Test scheitert, wenn diese Exception nicht geworfen wird
@Test(expected = java.lang.ArithmeticException.class)
public void testExceptAnnot() {
int i = 0 / 0;
}
In JUnit 5 hat die Annotation @Test
keinen expected
-Parameter mehr.
Als Alternative bietet sich der Einsatz von org.junit.jupiter.api.Assertions.assertThrows
an. Dabei benötigt man allerdings Lambda-Ausdrücke (Verweis auf spätere VL):
@Test
public void testExceptAnnot() {
assertThrows(java.lang.ArithmeticException.class, () -> {
int i = 0 / 0;
});
}
(Beispiel von oben mit Hilfe von JUnit 5 formuliert)
Parametrisierte Tests
Manchmal möchte man den selben Testfall mehrfach mit anderen Werten (Parametern) durchführen.
class Sum {
public int sum(int i, int j) {
return i + j;
}
}
class SumTest {
@Test
public void testSum() {
Sum s = new Sum();
assertEquals(s.sum(1, 1), 2);
}
// und mit (2,2, 4), (2,2, 5), ...????
}
Prinzipiell könnte man dafür entweder in einem Testfall eine Schleife schreiben, die über die verschiedenen Parameter iteriert. In der Schleife würde dann jeweils der Aufruf der zu testenden Methode und das gewünschte Assert passieren. Alternativ könnte man den Testfall entsprechend oft duplizieren mit jeweils den gewünschten Werten.
Beide Vorgehensweisen haben Probleme: Im ersten Fall würde die Schleife bei einem Fehler oder unerwarteten Ergebnis abbrechen, ohne dass die restlichen Tests (Werte) noch durchgeführt würden. Im zweiten Fall bekommt man eine unnötig große Anzahl an Testmethoden, die bis auf die jeweiligen Werte identisch sind (Code-Duplizierung).
Parametrisierte Tests mit JUnit 4
JUnit 4 bietet für dieses Problem sogenannte "parametrisierte Tests" an. Dafür muss eine Testklasse in JUnit 4 folgende Bedingungen erfüllen:
- Die Testklasse wird mit der Annotation
@RunWith(Parameterized.class)
ausgezeichnet. - Es muss eine öffentliche statische Methode geben mit der Annotation
@Parameters
. Diese Methode liefert eine Collection zurück, wobei jedes Element dieser Collection ein Array mit den Parametern für einen Durchlauf der Testmethoden ist. - Die Parameter müssen gesetzt werden. Dafür gibt es zwei Varianten:
a) Für jeden Parameter gibt es ein öffentliches Attribut. Diese Attribute
müssen mit der Annotation
@Parameter
markiert sein und können in den Testmethoden normal genutzt werden. JUnit sorgt dafür, dass für jeden Eintrag in der Collection aus der statischen@Parameters
-Methode diese Felder gesetzt werden und die Testmethoden aufgerufen werden. b) Alternativ gibt es einen Konstruktor, der diese Werte setzt. Die Anzahl der Parameter im Konstruktor muss dabei exakt der Anzahl (und Reihenfolge) der Werte in jedem Array in der von der statischen@Parameters
-Methode gelieferten Collection entsprechen. Der Konstruktor wird für jeden Parametersatz einmal aufgerufen und die Testmethoden einmal durchgeführt.
Letztlich wird damit das Kreuzprodukt aus Testmethoden und Testdaten durchgeführt.
Parametrisierte Tests mit JUnit 5
In JUnit 5 werden parametrisierte Tests mit der Annotation @ParameterizedTest
gekennzeichnet (statt mit @Test
).
Mit Hilfe von @ValueSource
kann man ein einfaches Array von Werten (Strings
oder primitive Datentypen) angeben, mit denen der Test ausgeführt wird. Dazu
bekommt die Testmethode einen entsprechenden passenden Parameter:
@ParameterizedTest
@ValueSource(strings = {"wuppie", "fluppie", "foo"})
void testWuppie(String candidate) {
assertTrue(candidate.equals("wuppie"));
}
Alternativ lassen sich als Parameterquelle u.a. Aufzählungen (@EnumSource
)
oder Methoden (@MethodSource
) angeben.
Hinweis: Parametrisierte Tests werden in JUnit 5 derzeit noch als "experimentell" angesehen!
Testsuiten: Tests gemeinsam ausführen (JUnit 4)
Eclipse: New > Other > Java > JUnit > JUnit Test Suite
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses({
// Hier kommen alle Testklassen rein
PersonTest.class,
StudiTest.class
})
public class MyTestSuite {
// bleibt leer!!!
}
Testsuiten mit JUnit 5
In JUnit 5 gibt es zwei Möglichkeiten, Testsuiten zu erstellen:
@SelectPackages
: Angabe der Packages, die für die Testsuite zusammengefasst werden sollen@SelectClasses
: Angabe der Klassen, die für die Testsuite zusammengefasst werden sollen
@RunWith(JUnitPlatform.class)
@SelectClasses({StudiTest5.class, WuppieTest5.class})
public class MyTestSuite5 {
// bleibt leer!!!
}
Zusätzlich kann man beispielsweise mit @IncludeTags
oder @ExcludeTags
Testmethoden mit bestimmten Tags
einbinden oder ausschließen. Beispiel: Schließe alle Tests mit Tag "develop" aus: @ExcludeTags("develop")
.
Dabei wird an den Testmethoden zusätzlich das Tag @Tag
verwendet, etwas @Tag("develop")
.
Achtung: Laut der offiziellen Dokumentation
(Abschnitt "4.4.4. Test Suite")
gilt zumindest bei der Selection über @SelectPackages
der Zwang zu einer Namenskonvention:
Es werden dabei nur Klassen gefunden, deren Name mit Test
beginnt oder endet!
Weiterhin werden Testsuites mit der Annotation @RunWith(JUnitPlatform.class)
nicht
auf der "JUnit 5"-Plattform ausgeführt, sondern mit der JUnit 4-Infrastuktur!
Wrap-Up
JUnit als Framework für (Unit-) Tests; hier JUnit 4 (mit Ausblick auf JUnit 5)
- Testmethoden mit Annotation
@Test
assert
(Testergebnis) vs.assume
(Testvorbedingung)- Aufbau der Testumgebung
@Before
- Abbau der Testumgebung
@After
- Steuern von Tests mit
@Ignore
oder@Test(timout=XXX)
- Exceptions einfordern mit
@Test(expected=package.Exception.class)
- Tests zusammenfassen zu Testsuiten
Schreiben Sie eine JUnit-Testklasse (JUnit 4.x oder 5.x) und testen Sie eine
ArrayList<String>
. Prüfen Sie dabei, ob das Einfügen und Entfernen wie
erwartet funktioniert.
-
Initialisieren Sie in einer
setUp()
-Methode das Testobjekt und fügen Sie zwei Elemente ein. Stellen Sie mit einer passendenassume*
-Methode sicher, dass die Liste genau diese beiden Elemente enthält. DiesetUp()
-Methode soll vor jedem Testfall ausgeführt werden. -
Setzen Sie in einer
tearDown()
-Methode das Testobjekt wieder aufnull
und stellen Sie mit einer passendenassume*
-Methode sicher, dass das Testobjekt tatsächlichnull
ist. DietearDown()
-Methode soll nach jedem Testfall ausgeführt werden. -
Schreiben Sie eine Testmethode
testAdd()
. Fügen Sie ein weiteres Element zum Testobjekt hinzu und prüfen Sie mit einer passendenassert*
-Methode, ob die Liste nach dem Einfügen den gewünschten Zustand hat: Die Länge der Liste muss 3 Elemente betragen und alle Elemente müssen in der richtigen Reihenfolge in der Liste stehen. -
Schreiben Sie eine Testmethode
testRemoveObject()
. Entfernen Sie ein vorhandenes Element (über die Referenz auf das Objekt) aus dem Testobjekt und prüfen Sie mit einer passendenassert*
-Methode, ob die Liste nach dem Entfernen den gewünschten Zustand hat: Die Liste darf nur noch das verbleibende Element enthalten. -
Schreiben Sie eine Testmethode
testRemoveIndex()
. Entfernen Sie ein vorhandenes Element über dessen Index in der Liste und prüfen Sie mit einer passendenassert*
-Methode, ob die Liste nach dem Entfernen den gewünschten Zustand hat: Die Liste darf nur noch das verbleibende Element enthalten. (Nutzen Sie zum Entfernen dieremove(int)
-Methode der Liste.) -
Schreiben Sie zusätzlich einen parametrisierten JUnit-Test für die folgende Klasse:
import java.util.ArrayList; public class SpecialArrayList extends ArrayList<String> { public void concatAddStrings(String a, String b) { this.add(a + b); } }
Testen Sie, ob die Methode
concatAddStrings
der KlasseSpecialArrayList
die beiden übergebenen Strings korrekt konkateniert und das Ergebnis richtig in die Liste einfügt. Testen Sie dabei mit mindestens den folgenden Parameter-Tripeln:a b expected "" "" "" "" "a" "a" "a" "" "a" "abc" "123" "abc123"
- [fernunihagenJunit] Einführung in JUnit
Thies, A. und Noelke, C. und Ungerc, Fernuniversität in Hagen. - [junit4] JUnit 5
The JUnit Team, 2022. - [Kleuker2019] Qualitätssicherung durch Softwaretests
Kleuker, S., Springer Vieweg, 2019. ISBN 978-3-658-24886-4. DOI 10.1007/978-3-658-24886-4. - [Osherove2014] The Art of Unit Testing
Osherove, R., Manning, 2014. ISBN 978-1-6172-9089-3. - [Spillner2012] Basiswissen Softwaretest
Spillner, A. und Linz, T., dpunkt, 2012. ISBN 978-3-86490-024-2. - [vogellaJUnit] JUnit 5 tutorial - Learn how to write unit tests
vogella GmbH, 2021.
Testfallermittlung: Wie viel und was muss man testen?
Mit Hilfe der Äquivalenzklassenbildung kann man Testfälle bestimmen. Dabei wird der Eingabebereich für jeden Parameter einer Methode in Bereiche mit gleichem Verhalten der Methode eingeteilt (die sogenannten "Äquivalenzklassen"). Dabei können einige Äquivalenzklassen (ÄK) gültigen Eingabebereichen entsprechen ("gültige ÄK"), also erlaubten/erwarteten Eingaben (die zum gewünschten Verhalten führen), und die restlichen ÄK entsprechen dann ungültigen Eingabebereichen ("ungültige ÄK"), also nicht erlaubten Eingaben, die von der Methode zurückgewiesen werden sollten. Jede dieser ÄK muss in mindestens einem Testfall vorkommen, d.h. man bestimmt einen oder mehrere zufällige Werte in den ÄK. Dabei können über mehrere Parameter hinweg verschiedene gültige ÄK in einem Testfall kombiniert werden. Bei den ungültigen ÄK kann dagegen immer nur ein Parameter eine ungültige ÄK haben, für die restlichen Parameter müssen gültige ÄK genutzt werden, und diese werden dabei als durch diesen Testfall "nicht getestet" betrachtet.
Zusätzlich entstehen häufig Fehler bei den Grenzen der Bereiche, etwa in Schleifen. Deshalb führt man zusätzlich noch eine Grenzwertanalyse durch und bestimmt für jede ÄK den unteren und den oberen Grenzwert und erzeugt aus diesen Werten zusätzliche Testfälle.
Wenn in der getesteten Methode der Zustand des Objekts eine Rolle spielt, wird dieser wie ein weiterer Eingabeparameter für die Methode betrachtet und entsprechend in die ÄK-Bildung bzw. GW-Analyse einbezogen.
Wenn ein Testfall sich aus den gültigen ÄK/GW speist, spricht man auch von einem "Positiv-Test"; wenn ungültige ÄK/GW genutzt werden, spricht man auch von einem "Negativ-Test".
- (K2) Merkmale schlecht testbaren Codes erklären
- (K2) Merkmale guter Unit-Tests erklären
- (K3) Erstellen von Testfällen mittels Äquivalenzklassenbildung und Grenzwertanalyse
Hands-On (10 Minuten): Wieviel und was muss man testen?
public class Studi {
private int credits = 0;
public void addToCredits(int credits) {
if (credits < 0) {
throw new IllegalArgumentException("Negative Credits!");
}
if (this.credits + credits > 210) {
throw new IllegalArgumentException("Mehr als 210 Credits!");
}
this.credits += credits;
}
}
JEDE Methode mindestens testen mit/auf:
- Positive Tests: Gutfall (Normalfall) => "gültige ÄK/GW"
- Negativ-Tests (Fehlbedienung, ungültige Werte) => "ungültige ÄK/GW"
- Rand- bzw. Extremwerte => GW
- Exceptions
=> Anforderungen abgedeckt (Black-Box)?
=> Wichtige Pfade im Code abgedeckt (White-Box)?
Praxis
- Je kritischer eine Klasse/Methode/Artefakt ist, um so intensiver testen!
- Suche nach Kompromissen: Testkosten vs. Kosten von Folgefehlern; beispielsweise kein Test generierter Methoden
=> "Erzeugen" der Testfälle über die Äquivalenzklassenbildung und Grenzwertanalyse (siehe nächste Folien). Mehr dann später im Wahlfach "Softwarequalität" ...
Äquivalenzklassenbildung
Beispiel: Zu testende Methode mit Eingabewert x, der zw. 10 und 100 liegen soll
-
Zerlegung der Definitionsbereiche in Äquivalenzklassen (ÄK):
- Disjunkte Teilmengen, wobei
- Werte einer ÄK führen zu gleichartigem Verhalten
-
Annahme: Eingabeparameter sind untereinander unabhängig
-
Unterscheidung gültige und ungültige ÄK
Bemerkungen
Hintergrund: Da die Werte einer ÄK zu gleichartigem Verhalten führen, ist es egal, welchen Wert man aus einer ÄK für den Test nimmt.
Formal hat man eine ungültige ÄK (d.h. die Menge aller ungültigen Werte). In der Programmierpraxis macht es aber einen Unterschied, ob es sich um Werte unterhalb oder oberhalb des erlaubten Wertebereichs handelt (Fallunterscheidung). Beispiel: Eine Funktion soll Werte zwischen 10 und 100 verarbeiten. Dann sind alle Werte kleiner 10 oder größer 100 mathematisch gesehen in der selben ÄK "ungültig". Praktisch macht es aber Sinn, eine ungültige ÄK für "kleiner 10" und eine weitere ungültige ÄK für "größer 100" zu betrachten ...
Traditionell betrachtet man nur die Eingabeparameter. Es kann aber Sinn machen, auch die Ausgabeseite zu berücksichtigen (ist aber u.U. nur schwierig zu realisieren).
Faustregeln bei der Bildung von ÄK
-
Falls eine Beschränkung einen Wertebereich spezifiziert: Aufteilung in eine gültige und zwei ungültige ÄK
Beispiel: Eingabewert x soll zw. 10 und 100 liegen
- Gültige ÄK: $[10, 100]$
- Ungültige ÄKs: $x < 10$ und $100 < x$
-
Falls eine Beschränkung eine minimale und maximale Anzahl von Werten spezifiziert: Aufteilung in eine gültige und zwei ungültige ÄK
Beispiel: Jeder Studi muss pro Semester an mindestens einer LV teilnehmen, maximal sind 5 LVs erlaubt.
- Gültige ÄK: $1 \le x \le 5$
- Ungültige ÄKs: $x = 0$ (keine Teilnahme) und $5 < x$ (mehr als 5 Kurse)
-
Falls eine Beschränkung eine Menge von Werten spezifiziert, die möglicherweise unterschiedlich behandelt werden: Für jeden Wert dieser Menge eine eigene gültige ÄK erstellen und zusätzlich insgesamt eine ungültige ÄK
Beispiel: Das Hotel am Urlaubsort ermöglicht verschiedene Freizeitaktivitäten: Segway-fahren, Tauchen, Tennis, Golf
- Gültige ÄKs:
- Segway-fahren
- Tauchen
- Tennis
- Golf
- Ungültige ÄK: "alles andere"
- Gültige ÄKs:
-
Falls eine Beschränkung eine Situation spezifiziert, die zwingend erfüllt sein muss: Aufteilung in eine gültige und eine ungültige ÄK
Hinweis: Werden Werte einer ÄK vermutlich nicht gleichwertig behandelt, dann erfolgt die Aufspaltung der ÄK in kleinere ÄKs. Das ist im Grunde die analoge Überlegung zu mehreren ungültigen ÄKs.
ÄKs sollten für die weitere Arbeit einheitlich und eindeutig benannt werden. Typisches Namensschema: "gÄKn" und "uÄKn" für gültige bzw. ungültige ÄKs mit der laufenden Nummer $n$.
ÄK: Erstellung der Testfälle
-
Jede ÄK durch mindestens einen TF abdecken
-
Dabei pro Testfall
- mehrere gültige ÄKs kombinieren, oder
- genau eine ungültige ÄK untersuchen (restl. Werte aus gültigen ÄK auffüllen; diese gelten dann aber nicht als getestet!)
Im Prinzip muss man zur Erstellung der Testfälle (TF) eine paarweise vollständige Kombination über die ÄK bilden, d.h. jede ÄK kommt mit jeder anderen ÄK in einem TF zur Ausführung.
Erinnerung: Annahme: Eingabeparameter sind untereinander unabhängig! => Es reicht, wenn jede gültige ÄK einmal in einem TF zur Ausführung kommt. => Kombination verschiedener gültiger ÄK in einem TF.
Achtung: Dies gilt nur für die gültigen ÄK! Bei den ungültigen ÄKs dürfen diese nicht miteinander in einem TF kombiniert werden! Bei gleichzeitiger Behandlung verschiedener ungültiger ÄK bleiben u.U. Fehler unentdeckt, da sich die Wirkungen der ungültigen ÄK überlagern!
Für jeden Testfall (TF) wird aus den zu kombinierenden ÄK ein zufälliger Repräsentant ausgewählt.
ÄK: Beispiel: Eingabewert x soll zw. 10 und 100 liegen
Äquivalenzklassen
Eingabe | gültige ÄK | ungültige ÄK |
---|---|---|
x | gÄK1: $[10, 100]$ | uÄK2: $x < 10$ |
uÄK3: $100 < x$ |
Tests
Testnummer | 1 | 2 | 3 |
---|---|---|---|
geprüfte ÄK | gÄK1 | uÄK2 | uÄK3 |
x | 42 | 7 | 120 |
Erwartetes Ergebnis | OK | Exception | Exception |
Grenzwertanalyse
Beobachtung: Grenzen in Verzweigungen/Schleifen kritisch
- Grenzen der ÄK (kleinste und größte Werte) zusätzlich testen
- "gültige Grenzwerte" (gGW): Grenzwerte von gültigen ÄK
- "ungültige Grenzwerte" (uGW): Grenzwerte von ungültigen ÄK
Zusätzlich sinnvoll: Weitere grenznahe Werte, d.h. weitere Werte "rechts" und "links" der Grenze nutzen.
Bildung der Testfälle:
- Jeder GW muss in mind. einem TF vorkommen
Pro TF darf ein GW (gültig oder ungültig) verwendet werden, die restlichen Parameter werden (mit zufälligen Werten) aus gültigen ÄK aufgefüllt, um mögliche Grenzwertprobleme nicht zu überlagern.
GW: Beispiel: Eingabewert x soll zw. 10 und 100 liegen
Äquivalenzklassen
Eingabe | gültige ÄK | ungültige ÄK |
---|---|---|
x | gÄK1: $[10, 100]$ | uÄK2: $x < 10$ |
uÄK3: $100 < x$ |
Grenzwertanalyse
Zusätzliche Testdaten: 9 (uÄK2o) und 10 (gÄK1u) sowie 100 (gÄK1o) und 101 (uÄK3u)
Tests
Testnummer | 4 | 5 | 6 | 7 |
---|---|---|---|---|
geprüfter GW | gÄK1u | gÄK1o | uÄK2o | uÄK3u |
x | 10 | 100 | 9 | 101 |
Erwartetes Ergebnis | OK | OK | Exception | Exception |
Hinweis: Die Ergebnisse der GW-Analyse werden zusätzlich zu den Werten aus der ÄK-Analyse eingesetzt. Für das obige Beispiel würde man also folgende Tests aus der kombinierten ÄK- und GW-Analyse erhalten:
Testnummer | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
geprüfte(r) ÄK/GW | gÄK1 | uÄK2 | uÄK3 | gÄK1u | gÄK1o | uÄK2o | uÄK3u |
x | 42 | 7 | 120 | 10 | 100 | 9 | 101 |
Erwartetes Ergebnis | OK | Exception | Exception | OK | OK | Exception | Exception |
Anmerkung: Analyse abhängiger Parameter
Wenn das Ergebnis von der Kombination der Eingabewerte abhängt, dann sollte man dies bei der Äquivalenzklassenbildung berücksichtigen: Die ÄK sind in diesem Fall in Bezug auf die Kombinationen zu bilden!
Schauen Sie sich dazu das Beispiel im [Kleuker2019], Abschnitt "4.3 Analyse abhängiger Parameter" an.
Die einfache ÄK-Bildung würde in diesem Fall versagen, da die Eingabewerte nicht unabhängig sind. Leider ist die Betrachtung der möglichen Kombinationen u.U. eine sehr komplexe Aufgabe ...
Analoge Überlegungen gelten auch für die ÄK-Bildung im Zusammenhang mit objektorientierter Programmierung. Die Eingabewerte und der Objektzustand müssen dann gemeinsam bei der ÄK-Bildung betrachtet werden!
Vergleiche [Kleuker2019], Abschnitt "4.4 Äquivalenzklassen und Objektorientierung".
Wrap-Up
- Gründliches Testen ist ebenso viel Aufwand wie Coden
- Äquivalenzklassenbildung und Grenzwertanalyse
Der RSV Flotte Speiche hat in seiner Mitgliederverwaltung (MitgliederVerwaltung
) die Methode
testBeitritt
implementiert. Mit dieser Methode wird geprüft, ob neue Mitglieder in den Radsportverein
aufgenommen werden können.
public class MitgliederVerwaltung {
/**
* Testet, ob ein Mitglied in den Verein aufgenommen werden kann.
*
* @param alter Alter in Lebensjahren, Bereich [0, 99]
* @param motivation Motivation auf einer Scala von 0 bis 10
* @return <code>true</code>, wenn das Mitglied aufgenommen werden kann, sonst <code>false</code>
*/
public boolean testBeitritt(int alter, int motivation) {
if (alter < 0 || alter > 99 || motivation < 0 || motivation > 10) {
throw new IllegalArgumentException("Alter oder/und Motivation ungültig");
}
if (alter < 16) {
return false;
}
return motivation >= 4 && motivation <= 7;
}
}
-
Führen Sie eine Äquivalenzklassenbildung durch und geben Sie die gefundenen Äquivalenzklassen (ÄK) an: laufende Nummer, Definition (Wertebereiche o.ä.), kurze Beschreibung (gültige/ungültige ÄK, Bedeutung).
-
Führen Sie zusätzlich eine Grenzwertanalyse durch und geben Sie die jeweiligen Grenzwerte (GW) an.
-
Erstellen Sie aus den ÄK und GW wie in der Vorlesung diskutiert Testfälle. Geben Sie pro Testfall (TF) an, welche ÄK und/oder GW abgedeckt sind, welche Eingaben Sie vorsehen und welche Ausgabe Sie erwarten.
Hinweis: Erstellen Sie separate (zusätzliche) TF für die GW, d.h. integrieren Sie diese nicht in die ÄK-TF.
-
Implementieren Sie die Testfälle in JUnit (JUnit 4 oder 5). Fassen Sie die Testfälle der gültigen ÄK in einem parametrisierten Test zusammen. Für die ungültigen ÄKs erstellen Sie jeweils eine eigene JUnit-Testmethode. Beachten Sie, dass Sie auch die Exceptions testen müssen.
- [fernunihagenJunit] Einführung in JUnit
Thies, A. und Noelke, C. und Ungerc, Fernuniversität in Hagen. - [junit4] JUnit 5
The JUnit Team, 2022. - [Kleuker2019] Qualitätssicherung durch Softwaretests
Kleuker, S., Springer Vieweg, 2019. ISBN 978-3-658-24886-4. DOI 10.1007/978-3-658-24886-4. - [Osherove2014] The Art of Unit Testing
Osherove, R., Manning, 2014. ISBN 978-1-6172-9089-3. - [Spillner2012] Basiswissen Softwaretest
Spillner, A. und Linz, T., dpunkt, 2012. ISBN 978-3-86490-024-2. - [vogellaJUnit] JUnit 5 tutorial - Learn how to write unit tests
vogella GmbH, 2021.
Mocking mit Mockito
Häufig hat man es in Softwaretests mit dem Problem zu tun, dass die zu testenden Klassen von anderen, noch nicht implementierten Klassen oder von zufälligen oder langsamen Operationen abhängen.
In solchen Situationen kann man auf "Platzhalter" für diese Abhängigkeiten zurückgreifen. Dies können einfache Stubs sein, also Objekte, die einfach einen festen Wert bei einem Methodenaufruf zurückliefern oder Mocks, wo man auf die Argumente eines Methodenaufrufs reagieren kann und passende unterschiedliche Rückgabewerte zurückgeben kann.
Mockito ist eine Java-Bibliothek, die zusammen mit JUnit das Mocking von Klassen in Java erlaubt. Man kann hier zusätzlich auch die Interaktion mit dem gemockten Objekt überprüfen und testen, ob eine bestimmte Methode mit bestimmten Argumenten aufgerufen wurde und wie oft.
- (K2) Begriffe: Mocking, Mock, Stub, Spy
- (K3) Erzeugen eines Mocks in Mockito
- (K3) Erzeugen eines Spies in Mockito
- (K3) Prüfen von Interaktion mit verify()
- (K3) Einsatz von ArgumentMatcher
Motivation: Entwicklung einer Studi-/Prüfungsverwaltung
Szenario
Zwei Teams entwickeln eine neue Studi-/Prüfungsverwaltung für die Hochschule. Ein Team modelliert dabei die Studierenden, ein anderes Team modelliert die Prüfungsverwaltung LSF.
-
Team A:
public class Studi { String name; LSF lsf; public Studi(String name, LSF lsf) { this.name = name; this.lsf = lsf; } public boolean anmelden(String modul) { return lsf.anmelden(name, modul); } public boolean einsicht(String modul) { return lsf.ergebnis(name, modul) > 50; } }
-
Team B:
public class LSF { public boolean anmelden(String name, String modul) { throw new UnsupportedOperationException(); } public int ergebnis(String name, String modul) { throw new UnsupportedOperationException(); } }
Team B kommt nicht so recht vorwärts, Team A ist fertig und will schon testen.
Wie kann Team A seinen Code testen?
Optionen:
- Gar nicht testen?!
- Das LSF selbst implementieren? Wer pflegt das dann? => manuell implementierte Stubs
- Das LSF durch einen Mock ersetzen => Einsatz der Bibliothek "mockito"
Motivation Mocking und Mockito
Mockito ist ein Mocking-Framework für JUnit. Es simuliert das Verhalten eines realen Objektes oder einer realen Methode.
Wofür brauchen wir denn jetzt so ein Mocking-Framework überhaupt?
Wir wollen die Funktionalität einer Klasse isoliert vom Rest testen können. Dabei stören uns aber bisher so ein paar Dinge:
- Arbeiten mit den echten Objekten ist langsam (zum Beispiel aufgrund von Datenbankenzugriffen)
- Objekte beinhalten oft komplexe Abhängigkeiten, die in Tests schwer abzudecken sind
- Manchmal existiert der zu testende Teil einer Applikation auch noch gar nicht, sondern es gibt nur die Interfaces.
- Oder es gibt unschöne Seiteneffekte beim Arbeiten mit den realen Objekten. Zum Beispiel könnte es sein, das immer eine E-Mail versendet wird, wenn wir mit einem Objekt interagieren.
In solchen Situationen wollen wir eine Möglichkeit haben, das Verhalten eines realen Objektes bzw. der Methoden zu simulieren, ohne dabei die originalen Methoden aufrufen zu müssen. (Manchmal möchte man das dennoch, aber dazu später mehr...)
Und genau hier kommt Mockito ins Spiel. Mockito hilft uns dabei, uns von den externen Abhängigkeiten zu lösen, indem es sogenannte Mocks, Stubs oder Spies anbietet, mit denen sich das Verhalten der realen Objekte simulieren/überwachen und testen lässt.
Aber was genau ist denn jetzt eigentlich Mocking?
Ein Mock-Objekt ("etwas vortäuschen") ist im Software-Test ein Objekt, das als Platzhalter (Attrappe) für das echte Objekt verwendet wird.
Mocks sind in JUnit-Tests immer dann nützlich, wenn man externe Abhängigkeiten hat, auf die der eigene Code zugreift. Das können zum Beispiel externe APIs sein oder Datenbanken etc. ... Mocks helfen einem beim Testen nun dabei, sich von diesen externen Abhängigkeiten zu lösen und seine Softwarefunktionalität dennoch schnell und effizient testen zu können ohne evtl. auftretende Verbindungsfehler oder andere mögliche Seiteneffekte der externen Abhängigkeiten auszulösen.
Dabei simulieren Mocks die Funktionalität der externen APIs oder Datenbankzugriffe. Auf diese Weise ist es möglich Softwaretests zu schreiben, die scheinbar die gleichen Methoden aufrufen, die sie auch im regulären Softwarebetrieb nutzen würden, allerdings werden diese wie oben erwähnt allerdings für die Tests nur simuliert.
Mocking ist also eine Technik, die in Softwaretests verwendet wird, in denen die gemockten Objekte anstatt der realen Objekte zu Testzwecken genutzt werden. Die gemockten Objekte liefern dabei bei einem vom Programmierer bestimmten (Dummy-) Input, einen dazu passenden gelieferten (Dummy-) Output, der durch seine vorhersagbare Funktionalität dann in den eigentlichen Testobjekten gut für den Test nutzbar ist.
Dabei ist es von Vorteil die drei Grundbegriffe "Mock", "Stub" oder "Spy", auf die wir in der Vorlesung noch häufiger treffen werden, voneinander abgrenzen und unterscheiden zu können.
Dabei bezeichnet ein
- Stub: Ein Stub ist ein Objekt, dessen Methoden nur mit einer minimalen Logik für den Test implementiert wurden. Häufig werden dabei einfach feste (konstante) Werte zurückgeliefert, d.h. beim Aufruf einer Methode wird unabhängig von der konkreten Eingabe immer die selbe Ausgabe zurückgeliefert.
- Mock: Ein Mock ist ein Objekt, welches im Gegensatz zum Stub bei vorher definierten Funktionsaufrufen mit vorher definierten Argumente eine definierte Rückgabe liefert.
- Spy: Ein Spy ist ein Objekt, welches Aufrufe und übergebene Werte protokolliert und abfragbar macht. Es ist also eine Art Wrapper um einen Stub oder einen Mock.
Mockito Setup
-
Gradle:
build.gradle
dependencies { implementation 'junit:junit:4.13.2' implementation 'org.mockito:mockito-core:4.5.1' }
-
Maven:
pom.xml
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> </dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.5.1</version> </dependency> </dependencies>
Manuell Stubs implementieren
Team A könnte manuell das LSF rudimentär implementieren (nur für die Tests, einfach mit festen Rückgabewerten): Stubs
public class StudiStubTest {
Studi studi; LSF lsf;
@Before
public void setUp() { lsf = new LsfStub(); studi = new Studi("Harald", lsf); }
@Test
public void testAnmelden() { assertTrue(studi.anmelden("PM-Dungeon")); }
@Test
public void testEinsicht() { assertTrue(studi.einsicht("PM-Dungeon")); }
// Stub für das noch nicht fertige LSF
class LsfStub extends LSF {
public boolean anmelden(String name, String modul) { return true; }
public int ergebnis(String name, String modul) { return 80; }
}
}
Problem: Wartung der Tests (wenn das richtige LSF fertig ist) und Wartung der Stubs (wenn sich die Schnittstelle des LSF ändert, muss auch der Stub nachgezogen werden).
Problem: Der Stub hat nur eine Art minimale Default-Logik (sonst könnte man ja das LSF gleich selbst implementieren). Wenn man im Test andere Antworten braucht, müsste man einen weiteren Stub anlegen ...
Mockito: Mocking von ganzen Klassen
Lösung: Mocking der Klasse LSF
mit Mockito für den Test von Studi
: mock()
.
public class StudiMockTest {
Studi studi; LSF lsf;
@Before
public void setUp() { lsf = mock(LSF.class); studi = new Studi("Harald", lsf); }
@Test
public void testAnmelden() {
when(lsf.anmelden(anyString(), anyString())).thenReturn(true);
assertTrue(studi.anmelden("PM-Dungeon"));
}
@Test
public void testEinsichtI() {
when(lsf.ergebnis("Harald", "PM-Dungeon")).thenReturn(80);
assertTrue(studi.einsicht("PM-Dungeon"));
}
@Test
public void testEinsichtII() {
when(lsf.ergebnis("Harald", "PM-Dungeon")).thenReturn(40);
assertFalse(studi.einsicht("PM-Dungeon"));
}
}
Der Aufruf mock(LSF.class)
erzeugt einen Mock der Klasse (oder des Interfaces) LSF
. Dabei wird ein Objekt
vom Typ LSF
erzeugt, mit dem man dann wie mit einem normalen Objekt weiter arbeiten kann. Die Methoden sind
allerdings nicht implementiert ...
Mit Hilfe von when().thenReturn()
kann man definieren, was genau beim Aufruf einer bestimmten Methode auf dem
Mock passieren soll, d.h. welcher Rückgabewert entsprechend zurückgegeben werden soll. Hier kann man dann für
bestimmte Argumentwerte andere Rückgabewerte definieren. when(lsf.ergebnis("Harald", "PM-Dungeon")).thenReturn(80)
gibt also für den Aufruf von ergebnis
mit den Argumenten "Harald"
und "PM-Dungeon"
auf dem Mock lsf
den Wert 80 zurück.
Dies kann man in weiten Grenzen flexibel anpassen.
Mit Hilfe der Argument-Matcher anyString()
wird jedes String-Argument akzeptiert.
Mockito: Spy = Wrapper um ein Objekt
Team B hat das LSF
nun implementiert und Team A kann es endlich für die Tests benutzen. Aber
das LSF
hat eine Zufallskomponente (ergebnis()
). Wie kann man nun die Reaktion des Studis
testen (einsicht()
)?
Lösung: Mockito-Spy als partieller Mock einer Klasse (Wrapper um ein Objekt): spy()
.
public class StudiSpyTest {
Studi studi; LSF lsf;
@Before
public void setUp() { lsf = spy(LSF.class); studi = new Studi("Harald", lsf); }
@Test
public void testAnmelden() { assertTrue(studi.anmelden("PM-Dungeon")); }
@Test
public void testEinsichtI() {
doReturn(80).when(lsf).ergebnis("Harald", "PM-Dungeon");
assertTrue(studi.einsicht("PM-Dungeon"));
}
@Test
public void testEinsichtII() {
doReturn(40).when(lsf).ergebnis("Harald", "PM-Dungeon");
assertFalse(studi.einsicht("PM-Dungeon"));
}
}
Der Aufruf spy(LSF.class)
erzeugt einen Spy um ein Objekt der Klasse LSF
. Dabei bleiben zunächst die Methoden
in LSF
erhalten und können aufgerufen werden, sie können aber auch mit einem (partiellen) Mock überlagert werden.
Der Spy zeichnet wie der Mock die Interaktion mit dem Objekt auf.
Mit Hilfe von doReturn().when()
kann man definieren, was genau beim Aufruf einer bestimmten Methode auf dem
Spy passieren soll, d.h. welcher Rückgabewert entsprechend zurückgegeben werden soll. Hier kann man analog zum Mock
für bestimmte Argumentwerte andere Rückgabewerte definieren. doReturn(40).when(lsf).ergebnis("Harald", "PM-Dungeon")
gibt also für den Aufruf von ergebnis
mit den Argumenten "Harald"
und "PM-Dungeon"
auf dem Spy lsf
den Wert
40 zurück.
Wenn man die Methoden nicht mit einem partiellen Mock überschreibt, dann wird einfach die originale Methode aufgerufen
(Beispiel: In studi.anmelden("PM-Dungeon")
wird lsf.anmelden("Harald", "PM-Dungeon")
aufgerufen.).
Auch hier können Argument-Matcher wie anyString()
eingesetzt werden.
Wurde eine Methode aufgerufen?
public class VerifyTest {
@Test
public void testAnmelden() {
LSF lsf = mock(LSF.class); Studi studi = new Studi("Harald", lsf);
when(lsf.anmelden("Harald", "PM-Dungeon")).thenReturn(true);
assertTrue(studi.anmelden("PM-Dungeon"));
verify(lsf).anmelden("Harald", "PM-Dungeon");
verify(lsf, times(1)).anmelden("Harald", "PM-Dungeon");
verify(lsf, atLeast(1)).anmelden("Harald", "PM-Dungeon");
verify(lsf, atMost(1)).anmelden("Harald", "PM-Dungeon");
verify(lsf, never()).ergebnis("Harald", "PM-Dungeon");
verifyNoMoreInteractions(lsf);
}
}
Mit der Methode verify()
kann auf einem Mock oder Spy überprüft werden, ob und wie oft und in welcher Reihenfolge
Methoden aufgerufen wurden und mit welchen Argumenten. Auch hier lassen sich wieder Argument-Matcher wie anyString()
einsetzen.
Ein einfaches verify(mock)
prüft dabei, ob die entsprechende Methode exakt einmal vorher aufgerufen wurde. Dies
ist äquivalent zu verify(mock, times(1))
. Analog kann man mit den Parametern atLeast()
oder atMost
bestimmte
Unter- oder Obergrenzen für die Aufrufe angeben und mit never()
prüfen, ob es gar keinen Aufruf vorher gab.
verifyNoMoreInteractions(lsf)
ist interessant: Es ist genau dann true
, wenn es außer den vorher abgefragten
Interaktionen keinerlei sonstigen Interaktionen mit dem Mock oder Spy gab.
LSF lsf = mock(LSF.class);
Studi studi = new Studi("Harald", lsf);
when(lsf.anmelden("Harald", "PM-Dungeon")).thenReturn(true);
InOrder inOrder = inOrder(lsf);
assertTrue(studi.anmelden("PM-Dungeon"));
studi.anmelden("Wuppie");
inOrder.verify(lsf).anmelden("Harald", "Wuppie");
inOrder.verify(lsf).anmelden("Harald", "PM-Dungeon");
Mit InOrder
lassen sich Aufrufe auf einem Mock/Spy oder auch auf verschiedenen Mocks/Spies in eine zeitliche
Reihenfolge bringen und so überprüfen.
Fangen von Argumenten
public class MatcherTest {
@Test
public void testAnmelden() {
LSF lsf = mock(LSF.class); Studi studi = new Studi("Harald", lsf);
when(lsf.anmelden(anyString(), anyString())).thenReturn(false);
when(lsf.anmelden("Harald", "PM-Dungeon")).thenReturn(true);
assertTrue(studi.anmelden("PM-Dungeon"));
assertFalse(studi.anmelden("Wuppie?"));
verify(lsf, times(1)).anmelden("Harald", "PM-Dungeon");
verify(lsf, times(1)).anmelden("Harald", "Wuppie?");
verify(lsf, times(2)).anmelden(anyString(), anyString());
verify(lsf, times(1)).anmelden(eq("Harald"), eq("Wuppie?"));
verify(lsf, times(2)).anmelden(argThat(new MyHaraldMatcher()), anyString());
}
class MyHaraldMatcher implements ArgumentMatcher<String> {
public boolean matches(String s) { return s.equals("Harald"); }
}
}
Sie können die konkreten Argumente angeben, für die der Aufruf gelten soll. Alternativ
können Sie mit vordefinierten ArgumentMatchers
wie anyString()
beispielsweise auf
beliebige Strings reagieren oder selbst einen eigenen ArgumentMatcher<T>
für Ihren
Typ T
erstellen und nutzen.
Wichtig: Wenn Sie für einen Parameter einen ArgumentMatcher
einsetzen, müssen Sie
für die restlichen Parameter der Methode dies ebenfalls tun. Sie können keine konkreten
Argumente mit ArgumentMatcher
mischen.
Sie finden viele weitere vordefinierte Matcher in der Klasse ArgumentMatchers
.
Mit der Klasse ArgumentCaptor<T>
finden Sie eine alternative Möglichkeit, auf
Argumente in gemockten Methoden zu reagieren. Schauen Sie sich dazu die Javadoc
von Mockito an.
Ausblick: PowerMock
Mockito sehr mächtig, aber unterstützt (u.a.) keine
- Konstruktoren
- private Methoden
- final Methoden
- static Methoden (ab Version 3.4.0 scheint auch Mockito statische Methoden zu unterstützen)
=> Lösung: PowerMock
Ausführlicheres Beispiel: WuppiWarenlager
Credits: Der Dank für die Erstellung des nachfolgenden Beispiels und Textes geht an @jedi101.
Bei dem gezeigten Beispiel unseres WuppiStores
sieht man, dass dieser
normalerweise von einem fertigen Warenlager die Wuppis beziehen möchte. Da
dieses Lager aber noch nicht existiert, haben wir uns kurzerhand einfach einen
Stub von unserem IWuppiWarenlager
-Interface erstellt, in dem wir zu
Testzwecken händisch ein Paar Wuppis ins Lager geräumt haben.
Das funktioniert in diesem Mini-Testbeispiel ganz gut aber, wenn unsere Stores
erst einmal so richtig Fahrt aufnehmen und wir irgendwann weltweit Wuppis
verkaufen, wird der Code des IWuppiWarenlagers
wahrscheinlich sehr schnell viel
komplexer werden, was unweigerlich dann zu Maintenance-Problemen unserer
händisch angelegten Tests führt. Wenn wir zum Beispiel einmal eine Methode
hinzufügen wollen, die es uns ermöglicht, nicht immer alle Wuppis aus dem Lager
zu ordern oder vielleicht noch andere Methoden, die Fluppis orderbar machen,
hinzufügen, müssen wir immer dafür sorgen, dass wir die getätigten Änderungen
händisch in den Stub des Warenlagers einpflegen.
Das will eigentlich niemand...
Einsatz von Mockito
Aber es gibt da einen Ausweg. Wenn es komplexer wird, verwenden wir Mocks.
Bislang haben wir noch keinen Gebrauch von Mockito gemacht. Das ändern wir nun.
Wie in diesem Beispiel gezeigt, müssen wir nun keinen Stub mehr von Hand erstellen, sondern überlassen dies Mockito.
IWuppiWarenlager lager = mock(IWuppiWarenlager.class);
Anschließend können wir, ohne die Methode getAllWuppis()
implementiert zu haben,
dennoch so tun als, ob die Methode eine Funktionalität hätte.
// Erstellen eines imaginären Lagerbestands.
List<String> wuppisImLager = Arrays.asList("GruenerWuppi","RoterWuppi");
when(lager.getAlleWuppis()).thenReturn(wuppisImLager);
Wann immer nun die Methode getAlleWuppis()
des gemockten Lagers aufgerufen
wird, wird dieser Aufruf von Mockito abgefangen und wie oben definiert
verändert. Das Ergebnis können wir abschließend einfach in unserem Test testen:
// Erzeugen des WuppiStores.
WuppiStore wuppiStore = new WuppiStore(lager);
// Bestelle alle Wuppis aus dem gemockten Lager List<String>
bestellteWuppis = wuppiStore.bestelleAlleWuppis(lager);
// Hat die Bestellung geklappt?
assertEquals(2,bestellteWuppis.size());
Mockito Spies
Manchmal möchten wir allerdings nicht immer gleich ein ganzes Objekt mocken, aber dennoch Einfluss auf die aufgerufenen Methoden eines Objekts haben, um diese testen zu können. Vielleicht gibt es dabei ja sogar eine Möglichkeit unsere JUnit-Tests, mit denen wir normalerweise nur Rückgabewerte von Methoden testen können, zusätzlich auch das Verhalten also die Interaktionen mit einem Objekt beobachtbar zu machen. Somit wären diese Interaktionen auch testbar.
Und genau dafür bietet Mockito eine Funktion: der sogenannte "Spy".
Dieser Spion erlaubt es uns nun zusätzlich das Verhalten zu testen. Das geht in die Richtung von BDD - Behavior Driven Development.
// Spion erstellen, der unser wuppiWarenlager überwacht.
this.wuppiWarenlager = spy(WuppiWarenlager.class);
Hier hatten wir uns einen Spion erzeugt, mit dem sich anschließend das Verhalten verändern lässt:
when(wuppiWarenlager.getAlleWuppis()).thenReturn(Arrays.asList(new Wuppi("Wuppi007")));
Aber auch der Zugriff lässt sich kontrollieren/testen:
verify(wuppiWarenlager).addWuppi(normalerWuppi);
verifyNoMoreInteractions(wuppiWarenlager);
Die normalen Testmöglichkeiten von JUnit runden unseren Test zudem ab.
assertEquals(1,wuppiWarenlager.lager.size());
Mockito und Annotationen
In Mockito können Sie wie oben gezeigt mit mock()
und spy()
neue
Mocks bzw. Spies erzeugen und mit verify()
die Interaktion überprüfen
und mit ArgumentMatcher<T>
bzw. den vordefinierten ArgumentMatchers
auf Argumente zuzugreifen bzw. darauf zu reagieren.
Zusätzlich/alternativ gibt es in Mockito zahlreiche Annotationen, die ersatzweise statt der genannten Methoden genutzt werden können. Hier ein kleiner Überblick über die wichtigsten in Mockito verwendeten Annotation:
-
@Mock
wird zum Markieren des zu mockenden Objekts verwendet.@Mock WuppiWarenlager lager;
-
@RunWith(MockitoJUnitRunner.class)
ist der entsprechende JUnit-Runner, wenn Sie Mocks mit@Mock
anlegen.@RunWith(MockitoJUnitRunner.class) public class ToDoBusinessMock {...}
-
@Spy
erlaubt das Erstellen von partiell gemockten Objekten. Dabei wird eine Art Wrapper um das zu mockende Objekt gewickelt, der dafür sorgt, dass alle Methodenaufrufe des Objekts an den Spy delegiert werden. Diese können über den Spion dann abgefangen/verändert oder ausgewertet werden.@Spy ArrayList<Wuppi> arrayListenSpion;
-
@InjectMocks
erlaubt es, Parameter zu markieren, in denen Mocks und/oder Spies injiziert werden. Mockito versucht dann (in dieser Reihenfolge) per Konstruktorinjektion, Setterinjektion oder Propertyinjektion die Mocks zu injizieren. Weitere Informationen darüber findet man hier: Mockito DokumentationAnmerkung: Es ist aber nicht ratsam "Field- oder Setterinjection" zu nutzen, da man nur bei der Verwendung von "Constructorinjection" sicherstellen kann, das eine Klasse nicht ohne die eigentlich notwendigen Parameter instanziiert wurde.
@InjectMocks Wuppi fluppi;
-
@Captor
erlaubt es, die Argumente einer Methode abzufangen/auszuwerten. Im Zusammenspiel mit Mockitosverify()
-Methode kann man somit auch die einer Methode übergebenen Argumente verifizieren.@Captor ArgumentCaptor<String> argumentCaptor;
-
@ExtendWith(MockitoExtension.class)
wird in JUnit5 verwendet, um die Initialisierung von Mocks zu vereinfachen. Damit entfällt zum Beispiel die noch unter JUnit4 nötige Initialisierung der Mocks durch einen Aufruf der MethodeMockitoAnnotations.openMocks()
im Setup des Tests (@Before
bzw.@BeforeEach
).
Prüfen der Interaktion mit verify()
Mit Hilfe der umfangreichen verify()
-Methoden, die uns Mockito mitliefert, können
wir unseren Code unter anderem auf unerwünschte Seiteneffekte testen. So ist es mit
verify
zum Beispiel möglich abzufragen, ob mit einem gemockten Objekt interagiert
wurde, wie damit interagiert wurde, welche Argumente dabei übergeben worden sind und
in welcher Reihenfolge die Interaktionen damit erfolgt sind.
Hier nur eine kurze Übersicht über das Testen des Codes mit Hilfe von Mockitos
verify()
-Methoden.
@Test
public void testVerify_DasKeineInteraktionMitDerListeStattgefundenHat() {
// Testet, ob die spezifizierte Interaktion mit der Liste nie stattgefunden hat.
verify(fluppisListe, never()).clear();
}
@Test
public void testVerify_ReihenfolgeDerInteraktionenMitDerFluppisListe() {
// Testet, ob die Reihenfolge der spezifizierten Interaktionen mit der Liste eingehalten wurde.
fluppisListe.clear();
InOrder reihenfolge = inOrder(fluppisListe);
reihenfolge.verify(fluppisListe).add("Fluppi001");
reihenfolge.verify(fluppisListe).clear();
}
@Test
public void testVerify_FlexibleArgumenteBeimZugriffAufFluppisListe() {
// Testet, ob schon jemals etwas zu der Liste hinzugefügt wurde.
// Dabei ist es egal welcher String eingegeben wurde.
verify(fluppisListe).add(anyString());
}
@Test
public void testVerify_InteraktionenMitHilfeDesArgumentCaptor() {
// Testet, welches Argument beim Methodenaufruf übergeben wurde.
fluppisListe.addAll(Arrays.asList("BobDerBaumeister"));
ArgumentCaptor<List> argumentMagnet = ArgumentCaptor.forClass(FluppisListe.class);
verify(fluppisListe).addAll(argumentMagnet.capture());
List<String> argumente = argumentMagnet.getValue();
assertEquals("BobDerBaumeister", argumente.get(0));
}
Wrap-Up
-
Gründliches Testen ist ebenso viel Aufwand wie Coden!
-
Mockito ergänzt JUnit:
- Mocken ganzer Klassen (
mock()
,when().thenReturn()
) - Wrappen von Objekten (
spy()
,doReturn().when()
) - Auswerten, wie häufig Methoden aufgerufen wurden (
verify()
) - Auswerten, mit welchen Argumenten Methoden aufgerufen wurden (
anyString
)
- Mocken ganzer Klassen (
Betrachten Sie die drei Klassen Utility.java
, Evil.java
und UtilityTest.java
:
public class Utility {
private int intResult = 0;
private Evil evilClass;
public Utility(Evil evilClass) {
this.evilClass = evilClass;
}
public void evilMethod() {
int i = 2 / 0;
}
public int nonEvilAdd(int a, int b) {
return a + b;
}
public int evilAdd(int a, int b) {
evilClass.evilMethod();
return a + b;
}
public void veryEvilAdd(int a, int b) {
evilMethod();
evilClass.evilMethod();
intResult = a + b;
}
public int getIntResult() {
return intResult;
}
}
public class Evil {
public void evilMethod() {
int i = 3 / 0;
}
}
public class UtilityTest {
private Utility utilityClass;
// Initialisieren Sie die Attribute entsprechend vor jedem Test.
@Test
void test_nonEvilAdd() {
Assertions.assertEquals(10, utilityClass.nonEvilAdd(9, 1));
}
@Test
void test_evilAdd() {
Assertions.assertEquals(10, utilityClass.evilAdd(9, 1));
}
@Test
void test_veryEvilAdd() {
utilityClass.veryEvilAdd(9, 1);
Assertions.assertEquals(10, utilityClass.getIntResult());
}
}
Testen Sie die Methoden nonEvilAdd
, evilAdd
und veryEvilAdd
der
Klasse Utility.java
mit dem JUnit- und dem
Mockito-Framework.
Vervollständigen Sie dazu die Klasse UtilityTest.java
und nutzen Sie
Mocking mit Mockito, um die Tests
zum Laufen zu bringen. Die Tests dürfen Sie entsprechend verändern, aber
die Aufrufe aus der Vorgabe müssen erhalten bleiben. Die Klassen Evil.java
und Utility.java
dürfen Sie nicht ändern.
Hinweis: Die Klasse Evil.java
und die Methode evilMethod()
aus
Utility.java
lösen eine ungewollte bzw. "zufällige" Exception aus,
auf deren Auftreten jedoch nicht getestet werden soll. Stattdessen
sollen diese Klassen bzw. Methoden mit Mockito "weggemockt" werden, so
dass die vorgegebenen Testmethoden (wieder) funktionieren.
- [Mockito] Mockito
S. Faber and B. Dutheil and R. Winterhalter and T.v.d. Lippe, 2022.