Morden mit Mockito

“Es ist eine Gabe… und ein Fluch!”

Adrian Monk

Einer der wichtigsten Frameworks, um Unit Tests in Java zu schreiben, ist wohl Mockito. Kaum ein Projekt kommt ohne die Möglichkeit aus, Abhängigkeiten in den Testmethoden durch Mock Objekte zu ersetzen.

Leider ist jedes hilfreiche Framework in der Software Entwicklung auch eine Pandora Büchse, gefüllt mit kreativen Möglichkeiten, einem Projekt zu schaden.

Bei dem Entwurf eines Unit Test sollte es keine externen Bezüge für den zu testenden Code geben. Im einfachsten Fall können solche Abhängigkeiten durch die Verwendung von Interfaces vermieden werden.

Anstelle der tatsächlichen Implementierungen können dann Testvarianten verwendet werden. Statt diese selbst zu erstellen, werden häufig dynamische erzeugte Mocks verwendet.

@Mock private PersonRepository repository;

@Test void fathers() {
  when(repository.findAll()).the return(ALL_PERSONS);
  assertEquals(23, service.getFathers().size());
  verify(repository).findAll();
}

Dieses kurze Beispiel zeigt die Anwendung von Mockito und seine extremen Vorteile für den Entwickler. Die Membervariable mit der @Mock Annotation, wird vor dem Test mit einer Mock-Instanz belegt. Durch den when Aufruf bekommt diese ein bestimmtes Verhalten vorgeschrieben.

Danach erfolgt der Aufruf der zu testenden Methode,in die der Mock verwendet wird. Am Ende des Tests kann mit verify überprüft werden, welche Methodenaufrufe auf der Mock-Instanz stattgefunden haben. All dies in eigene Test-Implementierungen selber einzubauen, bedeutet einen großen zusätzlichen Arbeitsaufwand.

Es gibt vier typische Fehler, die sich mit Mocks in die Test einschleichen. Es sind die Anti-Pattern Golden Hammer, Mockery, Excessive Setup und Sliders.

Das Auftauchen von Golden Hammer erkennt man sehr leicht, weil fast alles durch Mocks ersetzt wurde. Dies ist aus vielerlei Gründen schlecht. Der Code der Testklasse ist klassischer Spagetti Code mit globalen Variablen. Die Klassen enthalten häufig Methoden, die wie eine Schrotflinten, Verhalten auf die Mocks aufbringen. Dann ist nach einiger Zeit für niemanden mehr klar, welcher Test, welche Mocks und welches Verhalten auf diesen benötigt.

Normale POJOS, Optional Ergebnisse und Collections sollten grundsätzlich nicht gemockt werden. Das spezielle Mocken von Collection und Optional Klassen hat sogar den Namen Glas Hammer verdient, da hier schon ein einfaches Refactoring einen Test zerbrechen lassen kann. Für die folgende, hässliche Methode sollen Unit Test geschrieben werden.

Integer magic() {
  Optional<Integer>; result = repository.findById(42)
  return result.isPresent() ? result.get() : 0; 
}

Im Test werden dabei Repository Aufruf und Ergebnis gemocked.

when(optional.isPresent()).thenReturn(true);
when(optional.get(any()).thenReturn(21);
when(repository.findById(42)).thenReturn(optional);
assertEquals(21, magic());

Dieser Test ist zerbrechlich wie Glas, wenn ein Entwickler die Optional Verarbeitung ändert, ohne den Test anzupassen.

Integer magic() {
  return repository.findById(42).orElse(0); 
}

Solche partiell gemockten Hilfsobjekte sind im Initialisierungscode eines Unit Test schwer aufzuspüren und ihre Existenz widerspricht jeder Erwartung. Außerdem ist gerade die Idee des Refactoring, den Code so zu ändern, dass weiterhin alle Tests ohne Anpassung erfolgreich durchlaufen werden.

Daher ist es sinnvoller, solche Objekte erst gar nicht zu mocken. Insbesondere, wenn der Test dadurch viel lesbarer wird.

when(repository.findById(42)).thenReturn(Optional.of(21));
assertEquals(21, magic());

Werden häufig spezielle Domänen-Objekte in den Test benötigt, dann bieten sich hierfür eigene Parameter Resolver an.

Das Anti-Pattern Mockery beschreibt den Zustand, dass häufig nicht mehr klar ist, ob eine Funktionalität getestet wird, oder nur ein Ergebnis, dass von einem Mock erzeugt wurde. Prüft ein Test nur die Mocks, dann ist er sinnlos und erzeugt falsches Vertrauen. Manches Mal wird sogar ein Mock verwendet um andere Mocks zu initialisieren.

when(bean.getValue()).thenReturn(4);
when(repository.findById(bean.getId()).thenReturn(Collections.emptyList());

Ob hier Unkenntnis des Mockito Frameworks vorliegt oder die Entwickler die Kontrolle über ihren Code verloren haben, ist dann nicht immer genau zu verorten. Wenn unnötige Dinge gemocked werden, wie die Rückgabe der Default Values (Collections.emptyList()), spricht es eher für grob fahrlässige Unwissenheit.

Das Anti-Pattern Excessive Setup beschreibt die Situation, dass eine große Anzahl von Objekten erzeugt und konfiguriert werden muss, bevor ein Test gestartet werden kann. Dies liegt häufig an einem komplexen Geflecht von Abhängigkeiten, die nicht weggekapselt wurden. Durch den unübersichtlichen Einsatz vieler Mocks kann die selbe Situation heraufbeschwört werden.

Das Anti-Pattern Sliders, ist als solches, erst mit diesem Beitrag ins Leben getreten. Der Name bezieht sich auf die gleichnamige Fernsehserie. In der Serie reisen die Protagonisten durch alternative Welten, die unserer sehr ähneln, aber doch deutliche Diskrepanzen aufweisen.

@Mock private PersonRepository repository;

@Test void fathersWithOnlyWomen() {
  when(repository.findAll()).the return(null);
  assertNull(service.getFathers());
  verify(repository).findAll();
}

Die obige Methode ähnelt dem ersten Beispiel, hat jedoch einen kleinen Schönheitsfehler. Das definierte Verhalten hat nichts mit den realen Implementierungen zu tun. Das obige Spring Data Repository liefert niemals null zurück. Es wird immer eine Liste zurückgeliefert. Gibt es keine Treffer, dann ist diese Liste leer.

Im harmlosen Fall werden völlig nutzlose Test generiert. Wird test-driven gearbeitet, dann erblicken völlig abwegige Sonderfall-Behandlungen das Licht der Welt Und dies alles nur, weil die Entwickler kein Interesse zeigen, sich mit dem verwendeten API vertraut zu machen.

Als Fazit kann man nur das folgende Zitat zu Herzen nehmen und nur das zu mocken, was gar nicht anders zu ersetzen ist.

“Allein die Dosis macht, dass ein Ding kein Gift ist.”

Paracelsus

1 Gedanke zu „Morden mit Mockito“

Kommentare sind geschlossen.