Please find an English version of this blog post here.
„Braucht die Welt wirklich einen weiteren Artikel über JUnit , Mockito und PowerMock ?“ Ich hatte da durchaus so meine Zweifel vor dem Schreiben dieses Artikels. Denn es gibt im Netz wirklich eine Unmenge an Artikeln genau zu diesen Themen. Andererseits bringt jeder Artikel auch seine eigene Sichtweise auf das Themengebiet mit und kann so am Ende – zumindest für ein paar Leser – hoffentlich trotzdem hilfreich sein. Aber genug von diesen philosophischen Betrachtungen und hinein in die Welt der JUnit-Tests und der (Power-)Mock-Objekte.
Was ist überhaupt ein Mock-Objekt?
Dieser Absatz behandelt – in aller Kürze – die grundlegenden Konzepte von Mock-Objekten. Wer schon Erfahrungen in diesem Bereich hat – auch aus anderen Programmiersprachen als Java – kann diesen Abschnitt getrost überspringen.
Bei Unit-Tests geht es darum die Methoden einer Klasse isoliert zu testen. Aber unsere Klassen leben gewöhnlich nicht in Isolation. Sie benutzten Services und Methoden von anderen Klassen. Und diese Klassen nutzen ihrerseits wieder Services und Methoden von weiteren Klassen. Diese Klassen nennen wir Collaborator. Dies führt zu zwei grundlegenden Problemen:
- Externe Services lassen sich in der normalen Entwicklungsumgebung oft gar nicht (fehlerfrei) aufrufen, da diese Zugriff auf die Datenbank oder andere externe Systeme erfordern.
- JUnit-Tests sollen die Implementierung einer spezifischen Klasse testen. Werden hierbei Aufrufe anderer Klassen unserer Geschäftslogik erlaubt, so kann deren – ggf. fehlerhaftes – Verhalten unsere Ergebnisse beeinflussen. Das ist nicht das was wir wollen.
Hier betreten Mock- und PowerMock-Objekte die Bühne. Diese beiden Werkzeuge erlauben es uns die Collaborator der zu testenden Klasse „zu verstecken“ und durch sogenannte Mock-Objekte zu ersetzen. Hierbei ist Mockito besonders gut geeignet für alle Standard-Fälle während PowerMock für die harten Brocken gebraucht wird. Dies betrifft insbesondere das Mocken von statischen und privaten Methoden. Mehr Informationen zum Thema „Mocking“ findet man hier . Das ist sozusagen DER Standard-Artikel zu diesem Thema.
Mockito im Einsatz
Genug der Vorrede und direkt hinein in ein erstes Beispiel. In unserer fiktiven Beispielsanwendung gibt es eine Klasse ItemService
. Diese benutzt die Klasse ItemRepository
um Daten aus der Datenbank zu laden. Offensichtlich ist diese Repository-Klasse ein guter Kandidat, um durch einen Mock ersetzt zu werden. In der zu testenden Methode wird ein Element anhand seiner Id geladen. Dann wird das geladene Element noch weiter verarbeitet. Wir wollen nur diese Verarbeitungs-Logik testen, nicht jedoch den Zugriff auf die Datenbank.
1public class ItemServiceTest {
2
3 @Mock
4 private ItemRepository itemRepository;
5
6 @InjectMocks
7 private ItemService itemService;
8
9 @Before
10 public void setUp() throws Exception {
11 MockitoAnnotations.initMocks(this);
12 }
13
14 @Test
15 public void getItemNameUpperCase() {
16
17 //
18 // Given
19 //
20 Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
21 when(itemRepository.findById("it1")).thenReturn(mockedItem);
22
23 //
24 // When
25 //
26 String result = itemService.getItemNameUpperCase("it1");
27
28 //
29 // Then
30 //
31 verify(itemRepository, times(1)).findById("it1");
32 assertThat(result, is("ITEM 1"));
33 }
34}
Viele Projekte nutzen ein Framework zur Dependency Injection. Für die Beispiele in diesem Artikel wird das Spring Framework genutzt . Somit können wir auch die entsprechenden Annotationen von Mockito nutzen. (Diese funktionieren so auch mit anderen Dependency Injection Frameworks.) Mit @Mock
können wir Mock-Objekte in unsere JUnit Testklasse markieren. Diese werden der zu testenden Klasse über die @InjectMocks
-Annotation injiziert. Zusammen mit der Initialisierung der Mock-Objekte in der setup()
-Methode ist es so sehr einfach alles für die eigentlichen Test-Methoden vorzubereiten.
Das gesamte Beispiel-Projekt steht auf GitHub zur Verfügung . Dieses enthält nicht nur die Tests, sondern auch eine kleine Beispiel-Anwendung, welche die zu testende Funktionalität beinhaltet. Dabei werden auch noch weitere Aspekte von Mockito und PowerMock genutzt, die gar nicht alle in diesem Artikel erwähnt werden können. Dies bietet eine gute Möglichkeit Dinge auch selber auszuprobieren.
Mit unseren Mock-Objekten können wir uns nun daran machen für diese ein bestimmtes – sprich unser erwartetes – Verhalten zu erzeugen. Dies geschieht über eine spezielle Syntax, die auf den ersten Blick ein wenig gewöhnungsbedürftig erscheint:
wenn-Methodenaufruf-dann-Rückgabe
Übertragen auf unser obiges Beispiel:
1when(itemRepository.findById(“it1”)).thenReturn(mockedItem)
Der Methodenaufruf bezieht sich immer auf eine Methode in einer der genutzten Klassen, deren Verhalten unsere Tests nicht beeinflussen soll. Auch wenn das obige Beispiel ein wenig künstlich erscheint, so hilft es doch ein Gefühl für die benötigte Syntax zu bekommen. Bei Tests für komplexere Klassen und Methoden kann die Menge der benötigten Mock-Aufrufe durchaus entmutigend wirken. Auf der anderen Seite sollte dies auch ein weiterer Anreiz sein, Klassen und Methoden kurz zu halten und Verantwortlichkeiten klar zu trennen.
Zusätzlich zum reinen „Mocken“ von Methoden kann auch geprüft werden, dass Methoden auch tatsächlich aufgerufen wurden. In unserem obigen Beispiel erfolgt dies durch folgenden Aufruf:
1verify(itemRepository, times(1)).findById(“it1”)
Dies ist besonders nützlich, um den Programmablauf der zu testenden Klasse zu prüfen.
Mockito kann aber noch mehr …
… viel mehr als man in einem kurzen Artikel wirklich zeigen kann. Aber neben den grundlegenden Funktionen aus dem vorherigen Abschnitt wollen wir uns hier noch eine weitere sehr nützliche Funktion anschauen. Es geht darum Objekte zu verändern, die als Parameter an Mock-Objekte übergeben wurden. So wie ja auch die echte Implementierung ggf. Objekte verändern würde. Hierzu nutzen wir die doAnswer()
-Funktion. Schauen wir uns dies direkt anhand eines Beispiels an.
1@Test
2public void testPLZAddressCombinationIncludingHostValue() {
3
4 //
5 // Given
6 //
7 Customer customer = new Customer("204", "John Do", "224B Bakerstreet");
8
9 doAnswer(new Answer<Customer>() {
10 @Override
11 public Customer answer(InvocationOnMock invocation) throws Throwable {
12 Object originalArgument = (invocation.getArguments())[0];
13 Customer param = (Customer) originalArgument;
14 param.setHostValue("TestHostValue");
15 return null;
16 }
17 }).when(hostService).expand(any(Customer.class));
18
19 when(addressService.getPLZForCustomer(customer)).thenReturn(47891);
20 doNothing().when(addressService).updateExternalSystems(customer);
21
22 //
23 // When
24 //
25 String address = customerService.getPLZAddressCombinationIncludingHostValue(customer, true);
26
27 //
28 // Then
29 //
30 Mockito.verify(addressService, times(1)).updateExternalSystems(any(Customer.class));
31 assertThat(address, is("47891_224B Bakerstreet_TestHostValue"));
32}
Mit den bisher gezeigten Konzepten ist ein großer Teil der „normalen“ Anwendungsfälle abgedeckt. Ein wichtiger Aspekt fehlt jedoch noch: Was tun wir wenn unsere zu testende Klasse z.B. statische Methoden einer anderen Klasse benutzt? Die Antwort auf diese Frage ist vermutlich nicht schwer zu erraten :-).
PowerMock – Das Unmögliche Mocken 🙂
PowerMock bietet Lösungen für die harten Fälle bei denen Mockito an seine Grenzen stösst. Im Allgemeinen bedeutet dies das Mocken von statischen Methoden. Es ist aber auch möglich private Methoden und Constructor-Aufrufe zu mocken. In allen diesen Fällen sollte die erste Frage aber immer lauten, ob nicht etwas an der Implementierung der Anwendung optimiert werden kann. Manchmal sind diese Art Test aber einfach notwendig und dann ist es gut ein Werkzeug wie PowerMock zur Hand zu haben. PowerMock arbeitet mit Manipulation des Byte-Code und nutzt dafür einen eigenen JUnit-Runner. Die zu mockenden Klassen werden dabei über die @PrepareForTest
-Annotation angegeben. Am Einfachsten ist wieder der Blick auf ein Beispiel.
1@RunWith(PowerMockRunner.class)
2@PrepareForTest({StaticService.class})
3public class ItemServiceTest {
4
5 @Mock
6 private ItemRepository itemRepository;
7
8 @InjectMocks
9 private ItemService itemService;
10
11 @Before
12 public void setUp() throws Exception {
13 MockitoAnnotations.initMocks(this);
14 }
15
16 @Test
17 public void readItemDescriptionWithoutIOException() throws IOException {
18
19 //
20 // Given
21 //
22 String fileName = "DummyName";
23
24 mockStatic(StaticService.class);
25 when(StaticService.readFile(fileName)).thenReturn("Dummy");
26
27 //
28 // When
29 //
30 String value = itemService.readItemDescription(fileName);
31
32 //
33 // Then
34 //
35 verifyStatic(times(1));
36 StaticService.readFile(fileName);
37 assertThat(value, equalTo("Dummy"));
38 }
39}
Es ist hier schön zu sehen, dass die Syntax von PowerMock fast identisch ist mit der Syntax von Mockito. Der Grund hierfür liegt in einer speziellen API die PowerMock für das Zusammenspiel mit Mockito bereit stellt. Dies erkennt man auch gut bei einem Blick auf die Maven-Konfiguration des Beispiel-Projekts. Es wird nicht nur PowerMock sondern auch die zugehörige Mockito-API importiert. Dabei ist darauf zu achten, dass die Versionen zusammen passen. Unser Beispiel basiert noch auf der Version 1.6.4 von Mockito und PowerMock. Es macht aber sicherlich auch Sinn einen Blick auf die neue Version 2.0 von Mockito und PowerMock zu werfen.
1... 2<dependency> 3 <groupId>org.powermock</groupId> 4 <artifactId>powermock-module-junit4</artifactId> 5 <version>1.6.4</version> 6 <scope>test</scope> 7</dependency> 8 9<dependency> 10 <groupId>org.powermock</groupId> 11 <artifactId>powermock-api-mockito</artifactId> 12 <version>1.6.4</version> 13 <scope>test</scope> 14</dependency> 15...
Statische Methodenaufrufe müssen über die mockStatic()
-Methode ge-mocked werden. Auch die Überprüfung eines Methodenaufrufs wird mit PowerMock ein wenig anders gehandhabt. Ansonsten ist die Syntax jedoch identisch.
Natürlich ist es möglich – und manchmal auch nötig – Mockito und PowerMock in ein und demselben JUnit-Test zu verwenden.In so einem Fall ist es sicherlich hilfreich sich im Team abzusprechen, welche Methoden statisch importiert werden (z.B. Mockito-when) und welche voll-qualifiziert genutzt werden (z.B. PowerMockito.when).
Ein nützliches Feature von PowerMock ist die Möglichkeit an einen anderen JUnit-Runner zu delegieren. Dies geschieht mit Hilfe der @PowerMockRunnerDelegate
-Annotation. Ein Anwendungsbeispiel zeigt der folgende kurze Code-Schnipsel. Das komplette Beispiel findet sich wieder auf GitHub .
1@RunWith(PowerMockRunner.class)
2@PowerMockRunnerDelegate(Parameterized.class)
3@PrepareForTest({StaticService.class})
4public class RateServiceTest {
5...
6}
In diesem Beispiel wird an den Parametrized.class JUnit-Runner delegiert , während gleichzeitig PowerMock genutzt werden kann. Eine anderes Beispiel könnte das Delegieren an den SpringJUnit4ClassRunner.class sein .
Zusammenfassung
Nach einer kurzen Eingewöhnung bietet Mockito eine sehr einfache und lesbare API für die Nutzung von Mock-Objekten in JUnit-Tests. Da PowerMock eine sehr ähnliche API liefert kann es fast genauso wie Mockito selber genutzt werden. Das erleichtert die gleichzeitige Nutzung beider Werkzeuge enorm.
Es ist möglich die Tests sehr lesbar zu schreiben. Dies hängt jedoch – wie immer – zu einem guten Teil von den zu testenden Klassen ab. Ansonsten bleibt hier nicht viel mehr zu sagen als: Happy Mocking 🙂
Dieser Blog-Post ist auch im Softwerker Vol. 9 erschienen. Den Softwerker kostenlos abonnieren: www.dersoftwerker.de .
Weitere Beiträge
von Thomas Jaspers
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Thomas Jaspers
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.