Mockito Matinée

“We’ve discussed this, mornings are for coffee and contemplation.”

Jim Hopper

Die Bibliothek Mockito ist für Java Entwickler in der Testerstellung ein unverzichtbarer Begleiter geworden. Leider kann bei der Verwendung von Mock Objekten so mancher Fehler passieren. Einige Anti-Pattern zum Testen mit Mocks wurde dazu im Beitrag Morden mit Mockito besprochen. Häufig werden aber auch nützliche Möglichkeiten von Mockito übersehen und dadurch die Tests unnötig aufgebläht.

Die Mock Objekte simulieren das Verhalten der realen Objekte, daher muss vor der Verwendung das Verhalten definiert werden. Dazu gehört insbesondere die Rückgabewerte von Methodenaufrufen.

when(ancestorService.getParent(any()).thenReturn(new Person("Jens Kaiser"));

In diesem Beispiel liefert die Methode getParent immer die selbe Instanz der Klasse Person zurück. Ohne diese Zeile wäre das Ergebnis null. Dieses Verhalten ist aber nicht der Standard.

In nächsten Beispiel liefert die Methode getChildren eine Liste zurück und es wäre auch hier das Ergebnis null zu erwarten, wenn diese Zeile fortgelassen würde.

when(ancestorService.getChildren(any()).thenReturn(List.of(new Person("Jens Kaiser")));

Tatsächlich kommt Mockito der bekannten Faulheit der Entwickler entgegen und liefert für Wrapper, Collections und Optionals passende Leer-Instanzen zurück. Dies sind die Wrapper mit den Nullwerten ihrer primitiven Geschwister und leere Collections und Optionals. Da heißt es also aufgepasst für die Entwickler von Legacy Software, denn das Ergebnis null muss explizit gesetzt werden.

Manche Methoden die simuliert werden, verändern andere Objekte. Um dies mit der Methode thenReturn zu simulieren, können zwei Instanzen erzeugt werden, die eine Instanz wird genutzt vor dem simulierten Aufruf und die andere danach.

Person oldPerson = new Person("Jens Kaiser");
Person newPerson = new Person("Jens Kaiser");
newPerson.setId(1);
when(ancestorService.save(oldPerson).thenReturn(List.of(newPerson));

Dieser Ansatz ist unübersichtlich und fehleranfällig. Bei Änderungen der Datenstrukturen müssen immer zwei Instanzen angepasst werden. Mockito bietet mit der Methode thenAnswer die Möglichkeit auf die Parameter des Methodenaufrufes zu reagieren.

Person person = new Person("Jens Kaiser");
when(ancestorService.update(oldPerson).thenAnswer(a -> { Person p = a.getArgument(0); p.setId(1); return p;});

Die Methode thenAnswer erhält einen Parameter vom Typ Answer, der Zugriff auf die Parameter der Methode, die Methode und auf den Mock selbst erlaubt. Es scheint keinen großen Sinn zu machen, auch die aktuelle Mock Instanz zu erhalten, aber dazu folgt noch ein passendes Beispiel.

Werden diverse ähnliche Interaktionen mit einem Mock in einem Tests benötigt, dann werden sie häufig untereinander aufgeführt.

when(ancestorService.getParent(1).thenReturn(new Person("Jens Kaiser"));
when(ancestorService.getParent(2).thenReturn(new Person("Friedrich Magnus Kayser"));
when(ancestorService.getParent(3).thenReturn(new Person("Diedrich Kayser"));

Bei einzelnen Mocks ist dies kein großes Problem, sind aber mehrere Mocks involviert, dann kann dies unübersichtlich werden. Für den Fall, dass nur die Reihenfolge der Aufrufe und nicht der aktuelle Parameter für den Test relevant ist, gibt es eine Variante der Methode thenReturn. Diese besitzt einen VarArg Parameter, um mehrere Rückgabewerte zu spezifizieren. Die drei Zeilen reduzieren sich so auf eine einzige.

when(ancestorService.getParent(any()).thenReturn(new Person("Jens Kaiser"), new Person("Friedrich Magnus Kayser"), new Person("Diedrich Kayser"));

Manchmal wird ein Mock verwendet, weil ein Sonderfall getestet werden soll, der nicht so einfach auf einer realen Instanz nachstellbar ist. Wenn zusätzlich andere Methoden aufgerufen werden sollen, müssen diese auch gemocked werden. Da wäre es schön, wenn eine reale Instanz im Test verwendet wird. Dieses Dilemma ist mit Mockito einfach gelöst, denn neben den Mocks können auch Spies verwendet werden. Während Mocks völlig synthetisch sind, verstecken sich hinter Spys reale Instanzen. Solange Mockito bei einem Spy kein Verhalten beschreibt, wird die reale Methode aufgerufen.

when(ancestorService.getParent(any()).thenAnswer(a -> {
  int index = a.getArgument(0);
  Person person = (Person)a.callRealMethod();
  if (index == 3) {
    person.setFamilies((AncestorService)a.getMock()).getFamilies());
  }
  return a.callRealMethod();
});

In diesem Beispiel wird auf einem Spy das Verhalten der getParent Methode verändert. Es wird für alle Anfragen die Methode der realen Instanz aufgerufen um eine Person zu erhalten. Für die Anfrage nach Diedrich Kayser wird zusätzlich die Liste der Familien vom Spy abgerufen. Hier liefert also die getMock Methode keinen Mock sondern einen Spy.

Häufig wird im Test geprüft, ob diverse Aufrufe auf den Mock Instanzen mit den korrekten Werten erfolgt sind.

verify(partnerService).getParent(2);

In diesem Fall prüft Mockito, ob es einen Aufrufen der Methode getParent auf dem Mock partnerService mit dem Parameter 2 gab. Manchmal sind die Parameter aber sehr komplex und es sollen nur einige Details geprüft werden. In dieser Situation kann der Captor verwendet werden.

@Captor
private ArgumentCaptor<List<Person>> persons;

@Test
void batch() {
  exporter.batch(List.of(1, 2, 3, 4));
  verify(partnerSevice).update(persons.capture());
  assertEquals(4, persons.getValue().size());
  assertEquals("Diedrich Kayser, persons.getValue().get(3));
}

In diesem Beispiel wird ein ArgumentCaptor für einen Parameter vom Typ List<Person> verwendet. Statt im verify direkt die Liste zu prüfen, entführt der ArgumentCaptor den Parameter, damit dieser dann in den folgenden Assertions über persons.getValue() abgefragt werden kann.

Damit wäre der Beitrag über Mockito schon fast zu Ende, doch hat sich in diesen Beitrag eine didaktisch entschuldbare Lüge versteckt. Der Hinweis darauf, dass bis auf die bekannten Ausnahmen, Mockito den Wert null zurück liefert, ist nicht ganz richtig. Bei der Erzeugung eines Mocks kann das Default Verhalten des Mocks verändert werden.

@Mock
Bean bean1;

@Mock(answer=Answers.RETURNS_MOCKS)
Bean bean2;

@Mock(answer=Answers.RETURNS_SMART_NULLS)
Bean bean3;

@Mock(answer=Answers.RETURNS_SELF)
Bean bean4;

@Test
void self() {
  assertNull(bean1.self());

  assertNotNull(bean2.self());
  assertNotEquals(bean2, bean2.self());

  assertNotNull(bean3.self());
  assertNotEquals(bean3, bean3.self());
  assertThrows(SmartNullPointerException.class, () -> bean3.self().self());

  assertSame(bean4, bean4.self());
}

In diesem Beispiel werden vier unterschiedliche Mocks vom Typ Bean erzeugt. Durch die Verwendung des answer Attributes an der @Mock Annotation erhalten alle vier ein ganz unterschiedliches Verhalten.

Die erste Bean verwendet das Default Strategie RETURN_DEFAULTS, daher liefert die Methode self auf diesem Mock den Rückgabewert null.

Mit RETURNS_MOCKS liefert die Methode self auf dem zweiten Mock ein weiteres Mock. Daher ist hier der Wert nie null, aber die beiden Objekte auch nicht identisch.

Die dritte Bean verwendet die Strategie RETURNS_SMART_NULLS. Statt null wird bei dieser Strategie ein Objekt zurück geliefert, das bei seiner Verwendung eine SmartNullPointerException wirft. Diese liefert mehr Fehlerinformationen als die klassische NullPointerException.

Die letzte Bean liefert mit jedem Aufruf der Methode self sich selbst zurück. Die Strategie RETURNS_SELF ist insbesondere für das Mocken von Builder ideal.

Damit ist diese Lüge für Vorträge korrigiert und die Matinée vorbei.