JUnit Tests implementieren mit Mockito und PowerMock

Keine Kommentare

Please find an English version of this blog post here.


junit „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.

public class ItemServiceTest {
 
    @Mock
    private ItemRepository itemRepository;
 
    @InjectMocks
    private ItemService itemService;
 
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
 
    @Test
    public void getItemNameUpperCase() {
 
        //
        // Given
        //
        Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
        when(itemRepository.findById("it1")).thenReturn(mockedItem);
 
        //
        // When
        //
        String result = itemService.getItemNameUpperCase("it1");
 
        //
        // Then
        //
        verify(itemRepository, times(1)).findById("it1");
        assertThat(result, is("ITEM 1"));
    }
}

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.

mockito

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:

	when(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:

	verify(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.

@Test
public void testPLZAddressCombinationIncludingHostValue() {
 
    //
    // Given
    //
    Customer customer = new Customer("204", "John Do", "224B Bakerstreet");
 
    doAnswer(new Answer<Customer>() {
        @Override
        public Customer answer(InvocationOnMock invocation) throws Throwable {
            Object originalArgument = (invocation.getArguments())[0];
            Customer param = (Customer) originalArgument;
            param.setHostValue("TestHostValue");
            return null;
        }
    }).when(hostService).expand(any(Customer.class));
 
    when(addressService.getPLZForCustomer(customer)).thenReturn(47891);
    doNothing().when(addressService).updateExternalSystems(customer);
 
    //
    // When
    //
    String address = customerService.getPLZAddressCombinationIncludingHostValue(customer, true);
 
    //
    // Then
    //
    Mockito.verify(addressService, times(1)).updateExternalSystems(any(Customer.class));
    assertThat(address, is("47891_224B Bakerstreet_TestHostValue"));
}

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.

@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticService.class})
public class ItemServiceTest {
 
    @Mock
    private ItemRepository itemRepository;
 
    @InjectMocks
    private ItemService itemService;
 
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
 
    @Test
    public void readItemDescriptionWithoutIOException() throws IOException {
 
        //
        // Given
        //
        String fileName = "DummyName";
 
        mockStatic(StaticService.class);
        when(StaticService.readFile(fileName)).thenReturn("Dummy");
 
        //
        // When
        //
        String value = itemService.readItemDescription(fileName);
 
        //
        // Then
        //
        verifyStatic(times(1));
        StaticService.readFile(fileName);
        assertThat(value, equalTo("Dummy"));
    }
}

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.

...
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.6.4</version>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.6.4</version>
    <scope>test</scope>
</dependency>
...

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.

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Parameterized.class)
@PrepareForTest({StaticService.class})
public class RateServiceTest {
...
}

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.

Thomas Jaspers

Langjährige Erfahrung in agilen Softwareprojekten unter Einsatz von Java-Enterprise-Technologien. Schwerpunkt im Bereich der Testautomatisierung (Konzepte und Open-Source Werkzeuge).

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.