Happy Hour: Unit-Tests mit Mockito

Artikel als PDF herunterladen:
Download Happy Hour: Unit-Tests mit Mockito

Limette und MinzeMockito ist ein Mock-Framework für JUnit-Tests. Es erlaubt das einfache Mocking von Klassen, die Installation dieser Mocks als Kollaborateure der jeweiligen Class-Under-Test sowie die Verifikation aller Interaktionen zwischen diesen. Die Syntax von Mockito ist sehr intuitiv, Unit-Tests gelingen rasch, sind robust und gut lesbar. Zur Veranschaulichung diene dieses Beispiel:

public class BusinessService {

  @EJB
  private ProjectDAO projectDAO;

  public List<Project> getProjectByOwner(String name) {
     List<Project> result = projectDAO.getByOwner(name);
     return result;
  }
}

Mit Mockito kann man folgenden Test schreiben:

import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;
import static org.junit.Assert.*;

import org.mockito.*;
import org.junit.*;

@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceTest {

  @InjectMocks
  private BusinessService cut = new BusinessService();

  @Mock
  private ProjectDao projectDao;

  @Test
  private void testGetProjectByOwner() {
    String ownerName = "whoever";
    List<Project> projects = Arrays.asList(new Project());
    doReturn(projects).when(projectDao).getByOwner(anyString());

    List<Project> result = cut.getProjectsByOwner(ownerName);

    // Whitbox-Tests
    verify(projectDao).getByOwner(ownerName);

    // herkömmliche Blackbox-Tests
    assertEquals(1, result.size());
    assertEquals(projects, result);
  }
}

Der Test besteht aus drei Phasen. Die erste Phase wird „Stubbing“ genannt. Hier wird das Verhalten der Kollaborateure festgelegt. In der zweiten Phase wird der tatsächliche Business-Code in der Class-Under-Test („cut“) ausgeführt. Im Gegensatz zur realen Ausführung interagiert der Business-Code mit den Test-Mocks. In der dritten Phase wird verifiziert, ob der Business-Code korrekt ausgeführt wurde. Hierzu kann man die bekannten JUnit-Assertion verwenden, um das äußere Verhalten zu prüfen (Blackbox-Test). Zusätzlich lässt sich das innere Verhalten mittels Mock-Verifikation (Whitebox-Test) prüfen.

Auf die Mockito-API möchte ich an dieser Stelle nicht weiter eingehen und verweise auf die hervorragende Projekt-Dokumentation: JavaDoc: Mockito.class

Warum Mockito?

Jede Anwendung, die mehr als nur algorithmischen Code enthält, kann nur mit Blackbox-Tests nicht sinnvoll getestet werden. Natürlich existieren unzählige Projekte, die trotzig versuchen, den Gegenbeweis anzutreten. Genau solche Projekte haben in der Regel eine atemberaubend niedrige Testabdeckung. Keiner stört sich daran, dass nicht alle Tests laufen. Und, man erzählt sich Legenden über die Zeit, als noch die gesamte Testsuite in wenigen Minuten durchlief. Mit „Unit-Test“ ist in solchen Projekten meist „Integrationstest“ gemeint, da ohne aktive Datenbankverbindung kaum ein Test ausgeführt werden kann (Michael Feathers: A Set of Unit Testing Rules).

Mockito ist nicht das einzige Framework seiner Art. JMock und EasyMock gibt es schon deutlich länger und beide bieten eine ähnliche Funktionalität. Schaut man sich jedoch die Suchtrends an, scheinen die Sympathien eindeutig verteilt zu sein:

Der Trend ist so eindeutig, dass es wenig lohnt auf die Vorgänger-Frameworks einzuprügeln (Off-Topic: How to beat a dead horse).

Eine Frage des Stils

Mockito bietet drei verschiedene Stile an, mit deren Hilfe Unit-Tests geschrieben werden können. Der erste Stil entstammt der akademischen Rezeption und wird unter der Bezeichnung „Behavior Driven Development“ gehandelt. Das Mantra lautet kurz „Given/When/Then“. Mir persönlich ist dieser Stil zu umständlich. Eine kurze Demonstration in Anknüpfung an das Eingangsbeispiel:

import static org.mockito.BDDMockito.*;
import static org.mockito.Matchers.*;

@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceTest {

  @InjectMocks
  private BusinessService cut = new BusinessService();

  @Mock
  private ProjectDao projectDao;

  @Test
  private void testGetProjectByOwner() {
    String ownerName = "whoever";
    List<Project> projects = Arrays.asList(new Project());

    // given
    given(projectDao.getByOwner(anyString())).willReturn(projects);

    // when
    List<Project> result = cut.getProjectsByOwner(ownerName);

    // then    
    verify(projectDao).getByOwner(ownerName);
  }
}

Der zweite Stil kann mit „When-Then/Act/Verify“ beschrieben werden:

import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;

@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceTest {

  @InjectMocks
  private BusinessService cut = new BusinessService();

  @Mock
  private ProjectDao projectDao;

  @Test
  private void testGetProjectByOwner() {
    String ownerName = "whoever";
    List<Project> projects = Arrays.asList(new Project());

    // when-then
    when(projectDao.getByOwner(anyString()).thenReturn(projects);

    // act
    List<Project> result = cut.getProjectsByOwner(ownerName);

    // verify    
    verify(projectDao).getByOwner(ownerName);
  }
}

Der dritte Stil ist dem zweiten sehr änlich und kann als „Do-When/Act/Verify“ beschrieben werden:

import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;
import static org.junit.Assert.*;

@RunWith(MockitoJUnitRunner.class)
public class BusinessServiceTest {

  @InjectMocks
  private BusinessService cut = new BusinessService();

  @Mock
  private ProjectDao projectDao;

  @Test
  private void testGetProjectByOwner() {
    String ownerName = "whoever";
    List<Project> projects = Arrays.asList(new Project());

    // do-when
    doReturn(projects).when(projectDao).getByOwner(anyString());

    // act
    List<Project> result = cut.getProjectsByOwner(ownerName);

    // verify    
    verify(projectDao).getByOwner(ownerName);
  }
}

Meiner Meinung nach ist der dritte Stil den anderen Stilen überlegen:

  • Es setzt die Betonung auf den Rückgabewert.
  • Es besteht eine Akkordanz mit dem Muster
    <result> = <object>.<method>(<parameter>).
  • Er erlaubt eine einfachere Klammerung.
  • Bei Methoden, die „void“ zurückliefern, muss ohnehin auf diesen Stil ausgewichen werden.

Konventionen, Dos and Don’ts

Schon für die Verwendung von JUnit gibt es etablierte Konventionen. Sehr sinnvoll ist beispielsweise, alle Test-Methoden mit „test“ anfangen zu lassen – obwohl dies durch JUnit 4 nicht vorgeschrieben wird. Der Vorteil dieser Konvention liegt auf der Hand: Schaut man sich den Call-Tree einer Methode an, kann man sofort alle Test-Methoden identifizieren und aussortieren.

Bei Mockito sind meiner Meinung nach diese Konventionen sinnvoll:

Pro JUnit-Testklasse sollte genau eine Anwendungsklasse getestest werden. Zur Deklaration bietet sich folgendes Muster an:

@InjectMocks
private ClassWithBusinessCode cut = 
                new ClassWithBusinessCode(); // class under test

Alle Kollaborateure sollten in der Testklasse so deklariert werden, wie sie im Anwendungscode deklariert sind:

@Mock
private Logger logger;

@Mock
private ProjectDao projectDao;

Wenig sinnvoll sind Bennungen wie „mockedProjectDao“ oder „projectDaoMock„. Dies verschlechtert nur die Lesbarkeit und verwirrt den @InjectMocks-Mechanismus.

Verifikationen sollten sich auf das relevante Verhalten beschränken. Anstelle zu prüfen

verify(logger).debug("Skip deleting project");

ist es hier besser, den Nicht-Sachverhalt zu prüfen:

verify(projectDao, never()).removeProjectById(anyLong());

Negativ-Verifikation sollten – wenn möglich – mit any-Matchers arbeiten:

verify(logger, never()).error(anyString());
verify(logger, never()).error(anyString(), any(Throwable.class));

Würde man hier auf einen festen String prüfen, würde der Negativ-Test sehr einfach zu erfüllen sein.

Die Test-Methoden sollten so einfach wie möglich gehalten werden. Immer wiederkehrendes Stubbing lässt sich in einer @Before-Methode zusammenfassen.

Was in diesem Zusammenhang nur wenige zu wissen scheinen: Die Klasse java.util.Collections bietet zahlreiche Methoden, um sich kurz zu fassen: singelton(Object), singletonList(Object) oder singletonMap(Object, Object), um nur einige zu nennen.

Es ist oft besser, einen Logger zu spy-en statt zu mock-en, also:

@Spy
private Logger logger = 
        Logger.getLogger(ClassWithBusinessCode.class);

Dies hat gegenüber einem Mock den Vorteil, die tatsächlichen Log-Ausgaben im Bedarf lesen zu können.

En détail

Im Folgenden drei Beispiele für besondere Problemfälle.

Überprüfen von Methoden-Parametern

Die Verifikation von Methodenaufrufen kann auf verschiedene Weise erfolgen:

  1. mithilfe von any-Matchern:
    verify(projectDao)​.persist(any(Project.class))
  2. mittels equals-Vergleich:
    verify(projectDao)​.persist(expectedProject)
  3. mittels Matcher-Vergleich:
    verify(projectDao)​.persist(argThat(MyMatchers​.isTheSameAs(expectedProject)))
  4. mittels eines ArgumentCaptors:
    verify(projectDao)​.persist(​myProjectCaptor.capture())

Variante 1 prüft nicht den tatsächlichen Methoden-Parameter – auch nicht seinen Laufzeit-Typ. Sie ist nur syntaktischer Zucker zur Bestimmung der passenden Methodensignatur.

Variante 2 prüft den Methoden-Parameter mittels equals-Methode. Für diese Variante sollte daher die Klasse „Project“ eine sinnvoll implementierte equals-Methode besitzen.

Variante 3 basiert auf der Implementierung eines eigenen Hamcrest-Matchers: MyProjectMatcher extends BaseMatcher<Project>: JavaDoc: BaseMatcher

Variante 4 wird durch Mockito selbst bereitgestellt: JavaDoc: ArgumentCaptor

ArgumentCaptor<Project> myProjectCaptor = 
        ArgumentCaptor.forClass(Project.class);

Dieser ArgumentCaptor kann im Verify-Abschnitt genutzt werden, um an die tatsächlichen Methodenparameter zu gelangen:

verify(projectDao).persist(myProjectCaptor.capture());
Project project = myProjectCaptor.getValue();

Die Projekt-Instanz kann dann weiteren Prüfungen unterzogen werden.

Problematische Konstruktoren

Eine Voraussetzung für die Verwendung von Mockito ist, dass die Class-Under-Test für den Testfall instantiiert werden kann. Es können jedoch Fälle eintreten, wo dies nicht möglich ist – beispielsweise, wenn Fremdsysteme innerhalb des Konstruktors angesprochen werden. Das Objenesis-Framework hilft hier aus der Klemme. Da dieses Framework von Mockito selbst verwendet wird, sind keine neuen Abhängigkeiten zu delarieren: JavaDoc: ObjenesisHelper.newInstance(java.lang.Class)

@InjectMocks
private ClassWithBusinessCode cut = 
        ObjenesisHelper.newInstance(ClassWithBusinessCode.class);

Nachdem die problematische Klasse instantiiert ist, kann Mockito alle Mocks injizieren und der Test von Methoden dieser Klasse kann wie gewohnt vonstattengehen.

Statische Methoden

Mockito kann keine Interaktionen prüfen, an der statische Methoden beteiligt sind. Die beste Lösung ist hierfür, die betreffende Stelle des Codes umzuschreiben. Wenn dies nicht möglich ist, hilft die PowerMock-Bibliothek weiter. PowerMock ist kein eigenständiges Test-Framework. Es ist vielmehr ein Add-On für Mockito und EasyMock, um diverse Einschränkungen dieser Frameworks zu mildern. Powermock erlaubt es „final„-Modifier zu entkräften, Objekt-Instanziierungen zu mocken oder statische Methoden-Aufrufe zu behandeln. Dies wird möglich, indem PowerMock einen eigenen ClassLoader einbringt und mittels Bytecode-Engineering zur Laufzeit die problematischen Klassen modifiziert.

Als Beispiel diene das klassische Locator-Muster:

public void List getProjectByOwner(String name) {
   ProjectDao projectDao = DaoLocator.getProjectDao();
   List<Project> result = projectDAO.getByOwner(name);
   return result;
}

Der Test hierfür sieht wie folgt aus:

import static org.mockito.Mockito.*;
import static org.mockito.Matchers.*;
import static org.junit.Assert.*;

import org.mockito.*;
import org.junit.*;

import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

@RunWith(PowerMockRunner.class) // statt MockitoJUnitRunner.class
@PrepareForTest({DaoLocator.class})
public class BusinessServiceTest {

  @InjectMocks
  private BusinessService cut = new BusinessService();

  @Mock
  private ProjectDao projectDao;

  @Test
  private void testGetProjectByOwner() {
    PowerMockito.mockStatic(DaoLocator.class);
    when(DaoLocator.getProjectDao()).thenReturn(projectDao);

    String ownerName = "whoever";
    List<Project> projects = Arrays.asList(new Project());
    doReturn(projects).when(projectDao).getByOwner(anyString());

    List<Project> result = cut.getProjectsByOwner(ownerName);

    verify(projectDao).getByOwner(ownerName);
  }
}

Die Änderungen für PowerMock halten sich sehr in Grenzen. Die Erfahrung lehrt jedoch, dass PowerMock nur in kleinen Dosen zum Einsatz kommen sollte. Es beißt sich mit dem Class-Loading-Mechanismus von Log4J, es führt Tools zur Messung der Testabdeckung in die Irre und es verlangsamt die Testausführung um mindestens eine Größenordnung.

Fazit

Mockito ist äußerst einfach zu verwenden, die Fehlermeldung bei Verification-Failures sind sehr aufschlussreich, die Dokumenation ist gut verständlich, das Diskussionsforum ist belebt und Bug-Reports werden zeitnah bearbeitet. Stubbing als auch Verifikation müssen in keiner besonderen Reihenfolge geschehen, es gibt keinen merkwürdigen Replay-Modus und es müssen keine syntaktischen Handstände beim Schreiben von Testfällen gemacht werden. Das ist ein deutliches Alleinstellungsmerkmal unter den Testframeworks, getreu nach dem Motto „Mache Einfaches einfach und Kompliziertes machbar!“.

Noch wünschenswert wäre ein stabiles SPI für die Integration mit anderen Frameworks – insbesondere PowerMock. Die Integration von Dexmaker zur Unterstützung der Android-Plattform ist hier ein erster, begrüßenswerter Schritt.

Kommentare sind abgeschaltet.