Cocktail gefällig?

Unit Tests mit Mockito

Christian Robert
© S&S Media

Das konsequente Schreiben von Unit Tests scheitert in der Praxis an vielerlei Dingen. Mangelnde Motivation oder falscher Stolz („In meinem Code verirren sich keine Bugs“) sind zwei Beispiele. Oft macht einem jedoch auch die eigene bzw. die vorgefundene Architektur einen Strich durch die Rechnung. Eine gute Unit macht unter anderem aus, dass sie möglichst wenige Abhängigkeiten zum Rest des Systems hat und damit für sich genommen testbar ist. Mithilfe des Mocking-Frameworks Mockito können genau diese Abhängigkeiten einfach und komfortabel behandelt werden. Mockito gehört in den Werkzeugkasten jedes Java-Entwicklers.

Das konsequente Erstellen und Warten von Unit Tests scheitert in der Praxis aus einer ganzen Reihe an Gründen. Mangelnde Motivation oder falscher Stolz sind nur zwei Beispiele. Ein anderer häufiger Grund ist die eigene bzw. die vorgefundene Architektur. Eine gute Unit zeichnet sich dadurch aus, dass sie möglichst wenige Abhängigkeiten zum Rest des Systems hat und damit für sich genommen gut und einfach testbar ist. Nicht immer lässt sich dies jedoch erreichen.

Im schlimmsten Fall erbt man als Entwickler eine bestehende Codebasis, in der bereits die grundlegende Struktur derartig verworren ist, dass mit vertretbarem Aufwand nur wenige Verbesserungen zu erzielen sind – Spaghetticode aus BASIC-Zeiten lässt grüßen.

Doch auch bei guter Architektur lassen sich größere Abhängigkeitsketten nicht immer ganz vermeiden. Ein typischer Anwendungsfall wäre die Implementierung der Tests eines Servlets. Üblicherweise werden hier die vom Container bereitgestellten Implementierungen von HttpServletRequest bzw. HttpServletResponse verwendet. Diese sind jedoch nur innerhalb eines laufenden Containers vorhanden und können nicht ohne Weiteres in Unit Tests genutzt werden.

Wenn wir nicht die Möglichkeit haben, die Quellcodebasis so anzupassen, dass wir schnell und einfach Unit Tests für neue Funktionalitäten hinzufügen können, bleibt uns dennoch eine weitere Möglichkeit: Wir „mocken“ uns die Abhängigkeiten einfach aus dem System heraus. Mocks sind laut Wikipedia „Objekte, die als Platzhalter für echte Objekte innerhalb von Modultests verwendet werden“ [1]. Wir täuschen also – ähnlich dem Proxy-Pattern – unserem zu testenden Code Abhängigkeiten vor, deren eigentliche Implementierung wir bei der Testausführung selbst kontrollieren können. Wir nehmen also die Logik des abhängigen Ziels selbst in die Hand.

[ header = Seite 2: Beispiel Cola-Automat ]

Beispiel: Cola-Automat

Sehen wir uns an einem Anwendungsfall die Möglichkeiten an, die sich uns durch Mocking bieten. Als unterstützendes Framework hat sich im Java-Umfeld inzwischen Mockito [2] etabliert. Doch stehen auch hier diverse Alternativen, wie z. B. EasyMock [3] oder JMockit [4] zur Verfügung.

Als Demonstrationsprojekt wollen wir die Implementierung einer einfachen Cola-Automatensteuerung inklusive Unit Tests realisieren. Der Einfachheit halber nehmen wir an, dass unser Cola-Automat eine fixe Anzahl an Fächern anbietet, aus denen wir unsere Flaschen wählen können, sowie ein Kassenmodul, das uns Informationen darüber gibt, wie viel Geld der Kunde vor Auswahl des Fachs in den Automaten geworfen hat. Das Interface unserer Steuerungssoftware, das für die Verarbeitung von Eingaben, die der Benutzer am Automaten macht, könnte wie in Listing 1 aussehen. Die Implementierung dazu ist in Listing 2 zu sehen.

public interface VendingMachine {

  public void selectItem(int boxIndex) throws
  NotEnoughMoneyException,
  BoxEmptyException;

}
public class VendingMachineImpl implements VendingMachine {

  private CashBox cashBox = null;
  private Box[] boxes = null;

  public VendingMachineImpl(CashBox cashBox, Box[] boxes) {
    this.cashBox = cashBox;
    this.boxes = boxes;
  }

  @Override
  public void selectItem(int boxIndex) throws 
  NotEnoughMoneyException, BoxEmptyException {
    Box box = this.boxes[boxIndex];
    if(box.isEmpty()) {
      throw new BoxEmptyException();
    } else {
      int amountRequired = box.getPrice();
      if(amountRequired > this.cashBox.getCurrentAmount()) {
        throw new NotEnoughMoneyException();
      } else {
        box.releaseItem();
        this.cashBox.withdraw(amountRequired);
      }
    }
  }

}

Die fachliche Logik ist einfach zu erkennen: Wählt der Benutzer ein Fach aus, in dem keine Ware mehr verfügbar ist, so wird eine BoxEmptyException geworfen. Das Display am Automaten muss entsprechend mit einer Nachricht an den Kunden reagieren. Ist Ware in einem Fach vorhanden, so wird überprüft, ob der Kunde vor seiner Auswahl genügend Geld in den Automaten geworfen hat. Ist dies der Fall, so wird dem Fach über die Box#releaseItem-Methode mitgeteilt, dass der Kunde die Ware erhalten soll. Ist nicht genügend Geld eingezahlt worden, so wird dies über eine geworfene NotEnoughMoneyException signalisiert.

Als externe Abhängigkeiten, auf die unsere Steuerungssoftware angewiesen sind, haben wir hier die beiden Klassen CashBox (für das Kassenmodul) sowie Box (für die einzelnen Fächer). Diese Abhängigkeiten wollen wir jedoch bei der reinen Implementierung unserer Automatensoftware vernachlässigen bzw. davon ausgehen, dass sie entsprechend korrekt implementiert sind. Wie setzen wir dies also in einem Unit Test korrekt um? Damit wir die Logik unseres Automaten vollständig testen können, benötigen wir während eines Unit Tests beide Abhängigkeiten: Eine CashBox und mind. eine Box. Nehmen wir an, der Integrator aller Softwarebestandteile unseres Automaten hat uns die in Listing 3 zu sehenden Schnittstellenbeschreibungen zur Verfügung gestellt.

public interface CashBox {

  public int getCurrentAmount();

  public void withdraw(int amountRequired);

}
public interface CashBox {

  public int getCurrentAmount();

  public void withdraw(int amountRequired);

}

Für einen Unit Test, der den Positivfall überprüft (genügend Geld eingezahlt und ein volles Fach), könnten wir nun diese beiden Interfaces wie in Listing 5 und 6 implementieren. Unser Unit Test für diesen Fall sieht wie in Listing 7 aus.

public class FullBox implements Box {

  @Override
  public boolean isEmpty() {
    return false;
  }

  @Override
  public int getPrice() {
    return 42;
  }

  @Override
  public void releaseItem() {
  }

}
public class UnlimitedCashBox implements CashBox {

  @Override
  public int getCurrentAmount() {
    return Integer.MAX_VALUE;
  }

  @Override
  public void withdraw(int amountRequired) {
  }

}
import org.junit.Test;

public class VendingMachineImplTest {

  @Test
  public void simpleTest() throws Exception {

    CashBox cashBox = new UnlimitedCashBox();
    Box[] boxes = new Box[] { new FullBox() };
    VendingMachineImpl machine = new 
    VendingMachineImpl(cashBox, boxes);

    machine.selectItem(0);

  }

}
 

Läuft dieser Test erfolgreich durch, so haben wir den ersten Positivfall unserer Anwendung abgedeckt. Wird keine Exception geworfen, so können wir davon ausgehen, dass der Benutzer seine Waren korrekt erhalten hat.

Doch können wir das wirklich? Haben wir überprüft, dass tatsächlich über Box#releaseItem der Automat angewiesen wurde, die Ware herauszugeben? Haben wir überprüft, ob auch der Kontostand in der Kasse entsprechend angepasst wurde? Nein! Alles dies geht aus unserem Testfall zunächst nicht hervor.

Wir müssen daher weitere Überprüfungen einbauen. Zunächst wollen wir sicherstellen, dass bei korrektem Durchlaufen tatsächlich die releaseItems-Methode der Box aufgerufen wurde, die Ware also auch tatsächlich an den Kunden geliefert wurde. Wir müssen unsere Dummy-Klasse erweitern und auch den Test entsprechend anpassen (Listings 8, 9).

public class FullBox implements Box {

  boolean releaseItemsCalled = false;

  @Override
  public void releaseItem() {
    this.releaseItemsCalled = true;
  }

  ...

}
import org.junit.Assert;
import org.junit.Test;

public class VendingMachineImplTest {

  @Test
  public void simpleTest() throws Exception {

    FullBox box = new FullBox();
    CashBox cashBox = new UnlimitedCashBox();
    Box[] boxes = new Box[] { box };

    VendingMachineImpl machine = new 
      VendingMachineImpl(cashBox, boxes);
    machine.selectItem(0);

    Assert.assertTrue(box.releaseItemsCalled);

  }

}

Nun können wir nach Durchlaufen des Testfalls sicher sein, dass im positiven Fall auch der Kunde seine Waren erhalten hat. Ändert sich bei zukünftigen Modifikationen des Anwendungscodes hier etwas, so sind wir durch den Testfall abgesichert und erhalten eine entsprechende Fehlermeldung. Ähnliches können wir nun auch für die CashBox umsetzen (Listing 10).

public class UnlimitedCashBox implements CashBox {

  int amountWithdrawn = 0;

  @Override
  public void withdraw(int amountRequired) {
    this.amountWithdrawn += amountRequired;
  }

  ...

}
import org.junit.Assert;
import org.junit.Test;

public class VendingMachineImplTest {

  @Test
  public void simpleTest() throws Exception {

    FullBox box = new FullBox();
    UnlimitedCashBox cashBox = new UnlimitedCashBox();
    Box[] boxes = new Box[] { box };

    VendingMachineImpl machine = new 
    VendingMachineImpl(cashBox, boxes);
    machine.selectItem(0);

    Assert.assertTrue(box.releaseItemsCalled);
    Assert.assertEquals(42, cashBox.amountWithdrawn);

  }

}
 

Das Prinzip unserer Tests wird klar: Zunächst konfigurieren wir die Mock-Objekte, die die Abhängigkeiten unseres Systems repräsentieren – hier sind dies FullBox bzw. UnlimitedCashBox. Im zweiten Schritt führen wir die eigentliche Logik unseres „Systems under Test“ aus. Im dritten und letzten Schritt vergleichen wir dann den Zustand unserer Mock-Objekte mit dem von uns erwartetem Zustand.

Analog zu unserem ersten Positivtest können wir nun auch unsere Negativtests schreiben und Fälle abprüfen wie z. B. „Nicht genügend Geld in der Kasse“, „Ausgewähltes Fach ist leer“ und so weiter. Ebenfalls sollten mehrfache Aufrufe und damit zumindest ansatzweise der Anwendungslebenszyklus getestet werden. Hiermit lässt sich z. B. beantworten, ob tatsächlich bei mehrfach hintereinander ausgeführten Aktionen der Stand in der Kasse korrekt heruntergezählt wird.

Um dies jedoch korrekt abzubilden, reichen unsere beiden Mock-Klassen FullBox und UnlimitedCashBox nicht mehr aus. Wir müssen diese entweder weiter konfigurierbar machen (z. B. das Guthaben in der Kasse setzen können) oder aber weitere spezialisierte Implementierungen der Interfaces erstellen.

[ header = Seite 3: Mockito stellt sich vor ]

Mockito stellt sich vor

Beide Lösungen erzeugen eine Menge Code, der lediglich dazu dient, die benötigen Mock-Objekte zu konfigurieren und deren Zustand zu speichern. Um schnell und einfach verschiedenste Situationen testen zu können, erlaubt es uns Mockito, genau diese benötigten und konfigurierbaren Objekte „on the fly“ zu erzeugen, zu konfigurieren und zu verwenden. Sehen wir uns zunächst ein komplettes Beispiel an und analysieren anschließend im Detail, was genau uns Mockito hier bietet (Listing 12).

import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mockito;

public class VendingMachineImplTest {

  @Test
  public void testWithMockito() throws Exception {

    CashBox cashBox = Mockito.mock(CashBox.class);
    Mockito.when(cashBox.getCurrentAmount()).thenReturn(42);

    Box box = Mockito.mock(Box.class);
    Mockito.when(box.isEmpty()).thenReturn(Boolean.FALSE);
    Mockito.when(box.getPrice()).thenReturn(42);

    Box[] boxes = new Box[] { box };
    VendingMachineImpl machine = new 
     VendingMachineImpl(cashBox, boxes);
    machine.selectItem(0);

    Mockito.verify(box).releaseItem();
    Mockito.verify(cashBox).withdraw(Matchers.eq(42));

  }

Auch hier erkennen wir prinzipiell die bereits vorgestellte Struktur: Vorbereitung der Mock-Objekte, Ausführung der Anwendungslogiken und Kontrolle des Zustands nach der Ausführung. Anstatt jedoch die Implementierungen von Box bzw. CashBox über selbst erstellte Klassen zu nutzen, verwenden wir die statische Methode Mockito.mock. Diese erzeugt dynamisch zur Laufzeit eine Implementierung der übergebenen Klasse, ähnlich den dynamischen Proxies der Java Virtual Machine [5]. Dies erkennen wir auch bei der Ausgabe der entsprechenden Objekte auf der Konsole:

CashBox cashBox = Mockito.mock(CashBox.class);
System.out.println(cashBox.getClass().getName());
// CashBox$$EnhancerByMockitoWithCGLIB$$7b3008fa

Direkt aus diesem Feature folgt, dass Mockito nur für solche Klassen Mock-Objekte erstellen kann, die auch sonst nach den Standard-Java-Regeln abgeleitet werden können. Ein Aufruf von Mockito.mock auf einer als final deklarierten Klasse führt daher zu folgender Fehlermeldung:

org.mockito.exceptions.base.MockitoException: 
Cannot mock/spy class FinalClass
Mockito cannot mock/spy following:
  - final classes
  - anonymous classes
  - primitive types

[ header = Seite 4: Konfiguration ]

Konfiguration

Nachdem die entsprechenden Objekte mittels Mockito.mock erzeugt wurden, lassen sie sich durch weitere statische, in der Klasse Mockito verfügbare Methoden weiter konfigurieren. In unserem Beispiel definieren wir z. B. über den Aufruf von Mockito.when die Implementierung der getCurrentAmount-Methode. Als Aktion beim Aufruf der Methode hinterlegen wir hierbei die Rückgabe des Werts „42“. Somit kann ohne großen Aufwand für einen Test das gewünschte Verhalten einer Abhängigkeit definiert werden. Neben der bereits gesehenen thenReturn-Aktion können wir auch eine Exception erzeugen und werfen. Dies könnten wir z. B. dazu nutzen, die Reaktion auf einen unerwarteten Fehler, der aus der CashBox gemeldet wird, zu kontrollieren:

CashBox cashBox = Mockito.mock(CashBox.class);
Mockito
  .doThrow(new RuntimeException("Internal error!"))
  .when(cashBox).withdraw(Matchers.anyInt());

Hier teilen wir Mockito mit, dass für unser erzeugtes Mock-Objekt immer dann eine Exception aus der withdraw-Methode geworfen werden soll, wenn die Methode mit einem beliebigen Integer-Wert als Parameter aufgerufen wird.

Über eigene Matcher-Implementierungen können noch feingranularere Bedingungen gesetzt werden. Alle Möglichkeit aufzuzeigen würde diesen Artikel bei Weitem sprengen, daher hier nur ein Beispiel eines eigenen Matchers, der immer dann aktiv wird, wenn der übergebene Wert negativ und damit ungültig ist (Listing 13).

Mockito
  .doThrow(new IllegalArgumentException("Invalid value!"))
  .when(cashBox).withdraw(Matchers.argThat(
    new ArgumentMatcher<Integer>() {
      @Override public boolean matches(Object argument) {
        return (int)argument < 0;
      }
    }
  )
);

[ header = Seite 5: Auswertung ]

Auswertung

Nachdem alle Mock-Objekte entsprechend konfiguriert wurden, kann der eigentliche Test beginnen. Dieser nutzt die Mock-Objekte wie die tatsächlichen Objekte. Mockito merkt sich intern jedoch alle auf den Mock-Objekten aufgerufenen Methoden sowie deren Parameter. So können wir im dritten Schritt, der Verifikation unserer Annahmen nach Ausführung des eigentlichen Tests, kontrollieren, ob auch alle Methoden der Mock-Objekte mit den entsprechenden Parametern aufgerufen wurden. Der Aufruf

Mockito.verify(cashBox).withdraw(Matchers.eq(42));

stellt beispielsweise sicher, dass die Methode withdraw unseres CashBox-Objekts genau einmal mit dem Parameter „42“ aufgerufen wurde. Ist dies nicht der Fall, so wird eine entsprechende Exception ausgelöst:

Wanted but not invoked:
cashBox.withdraw(42);
-> at VendingMachineImplTest.testWithMockito(VendingMachineImplTest.java:27)

Auch hier können über entsprechende Matcher-Implementierungen eigene Vergleiche schnell und einfach implementiert und genutzt werden. Als zweiter optionaler Parameter kann die verify-Methode noch mit der erwarteten Anzahl der Aufrufe konfiguriert werden. Im obigen Beispiel musste die Methode genau einmal aufgerufen werden. Wollen wir dies anpassen und z. B. kontrollieren, dass die Methode zweimal aufgerufen wird, so können wir den folgenden Aufruf nutzen:

Mockito
  .verify(cashBox, Mockito.times(2))
  .withdraw(Matchers.eq(42));

Genauso erlaubt es uns Mockito, sicherzugehen, dass keine bzw. keine weiteren Aktionen auf unseren Mock-Objekten durchgeführt wurden. Wollen wir beispielweise sicherstellen, dass unser System und der Test niemals mit einem bestimmten Mock agieren, so könnten wir den folgenden Testfall formulieren (Listing 14).

public class NotUsedTest {

  @Test
  public void testParameterNotUsed() {

    Foo mockedFoo = Mockito.mock(Foo.class);

    Bar bar = new Bar();
    bar.doStuff(mockedFoo);

    Mockito.verifyZeroInteractions(mockedFoo);

  }

}

[ header = Seite 6: Spione ]

Spione

Bisher haben wir uns die Mock-Objekte immer komplett von Mockito erzeugen lassen. Manchmal wollen wir jedoch nur bestimmte Methoden gesondert behandeln und für den Rest ein tatsächliches Objekt benutzen. Auch hierzu bietet uns Mockito ein entsprechendes Konstrukt an: Wir können mit Mockito.spy einen „Spion“ auf ein existierendes Objekt erzeugen lassen. Dieser Spion dient als Wrapper und leitet, sofern nichts anderes konfiguriert wurde, alle Methodenaufrufe an ein echtes Objekt weiter (Listing 15).

public class SpyObject {

  public String sayHello() {
    return "Hello!";
  }

  public static void main(String[] args) {

    SpyObject real = new SpyObject();
    SpyObject spy = Mockito.spy(real);
    SpyObject anotherSpy = Mockito.spy(real);
    
    Mockito
      .when(anotherSpy.sayHello())
      .thenReturn("Goodbye!");

    System.out.println("1: " + real.sayHello());
    System.out.println("2: " + spy.sayHello());
    System.out.println("3: " + anotherSpy.sayHello());

  }

}

Die Ausgabe aus Listing 15 lautet:

1: Hello!

2: Hello!

3: Goodbye!

Wie wir sehen, haben wir erfolgreich lediglich die Methode sayHello eines Spions dynamisch zur Laufzeit überschrieben.

[ header = Seite 7: Fazit ]

Fazit

Mock-Objekte erleichtern das Leben auf verschiedene Art und Weise. Auf der einen Seite erlauben sie es, externe Systembestandteile einfach und komfortabel zu simulieren und je nach Einsatzzweck überhaupt erst für Unit Tests verfügbar und nutzbar zu machen. Auf der anderen Seite reduzieren Frameworks wie Mockito die Codemenge, die für Unit Tests benötigt wird, teils erheblich und tragen somit zu einer besseren Lesbarkeit und Wartbarkeit unseres Testcodes bei.

Natürlich kommt auch dieses Feature nicht ohne Risiken und Nebenwirkungen: Die Implementierung der Mock-Objekte erfolgt programmatisch „on the fly“ und kann, gerade in komplexeren Szenarien, nicht immer einfach zu erfassen sein. Es gilt daher in der Praxis immer zu überprüfen, ob durch die Verwendung eines Mocking-Frameworks wie Mockito tatsächlich Übersichtlichkeit gewonnen werden kann oder nicht in bestimmten Fällen auch ein anderer Weg in Betracht gezogen werden sollte.

Für den schnellen Aufbau verschiedener Parameterkombinationen in einfachen Unit Tests ist Mockito mit seiner einfachen und klaren Syntax aber geradezu ideal geeignet und sollte im Werkzeugkasten eines jeden Java-Entwicklers zu finden sein.

Geschrieben von
Christian Robert
Christian Robert
Christian Robert ist Senior Developer für Mobile Lösungen bei SapientNitro in Köln. Seit über zehn Jahren beschäftigt er sich mit der Konzeption und Entwicklung von Individualsoftware im Java-Umfeld. Seine aktuellen Schwerpunkte liegen in der Entwicklung von pragmatischen und dennoch (oder gerade deswegen) effizienten Softwarelösungen im mobilen Umfeld. Außerdem interessiert er sich intensiv für die Ideen der Software Craftsmanship Bewegung.
Kommentare
  1. Yves Bossel2013-12-11 21:01:29

    Mockito is einfacher als EasyMock und JMock.

    Aber für jede Mock Framework muss man sich vorbereiten:
    - keine Abhängigkeit (setters, static method, new Irgendwas(...), usw.)
    - besser: Abhängigkeiten im Konstruktor lösen. So kann man die Mockobjecten direkt durch Instanziierung festlegen.
    - Um alle Abhängigkeiten im Konstruktor festzulegen, braucht man ein (kleines) Framework, das sie in einem einzigen Punkt zusammensetzet. (In der Nähe vom "main" für Programme, von "execute" für Servlets, "von" ejbCreate für EJBs, usw.)

    Unit Testing, Mockups arbeiten sehr gut mit Constructor Dependency Injection (z.B. http://picocontainer.codehaus.org/)

    Gute Übung: Programmieren Sie ohne Abhängigkeiten: entfernen Sie alle "static" und "new" aus Ihr Lieblingsprogramm. Sie werden sehen, dass die es kleiner und klarer wird, und nur Businesslogik enthält.

  2. Max Mustermann2014-07-15 12:08:26

    Hallo,

    tolle erklärung ! Danke.
    Aber in Listings 3 ist Ihnen ein Fehler unterlaufen.
    Sie haben drine falschen Interface, nämlich von CashBox.

  3. Max Mustermann22017-08-03 10:10:20

    Danke für die Doku. Allerdings muss ich sagen, dass 4 Jahren schon vorbei sind und der Fehler in Listing 3 ist immer noch da. Wie viele Jahren brauchen Sie noch?

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.