Mockito – Der Spion der mich hasste

Haben Sie explodierende Stifte erwartet? So etwas machen wir eigentlich nicht mehr.“

Q

Die Bibliothek Mockito ist in der Test-Driven Software Entwicklung kaum noch wegzudenken. Die Möglichkeit, komplexe Interaktionen mit APIs durch Mocks simulieren zu lassen, vereinfacht und beschleunigt die Implementierung eigener Anwendungen. Leider gibt es aber immer wieder kleine Stolperfallen in der Verwendung dieses ansonsten hervorragenden Frameworks.

Die Mockito Annotationen für Unit Tests bereitstellt erleichtern die Konfiguration der zu testenden Klassen. Statt aufwendigen Konfigurations-Code zu erstellen, erzeugt Mockito die notwendigen Instanzen und fügt sie zusammen.

Im folgenden Beispiel werden der Mock myMock und der Spy mySpy und die zu testende Instanz testee erzeugt und dabei myMock und mySpy in testee eingefügt. Wie dies genau geschieht ist hier nicht wichtig, aber auch dabei kann so einiges schief laufen.

@Mock
private MyMockedService myMock;

@Spy 
private MySpiedService mySpy;

@InjectMocks 
private MyClassUnderTest testee;

Wird nun die Instanz testee verwendet, dann verwendet diese Methoden der beiden anderen Instanzen. Der Unterschied zwischen einem Mock und einem Spy ist dabei aber zu beachten. Obwohl beide das Verhalten der tatsächlichen Implementierung überschreiben können unterscheiden sie sich in ihrem Standardverhalten. Der Mock eine virtuelle Instanz und alle Methodenaufrufe, die für die Tests nicht überschrieben wurden, liefern ein leeres Ergebnis. Der Spy greift aber auf eine echte Instanz der Klasse zurück. Alle nicht überschriebenen Methodenaufrufe werden an die echte Instanz weitergereicht und von dieser ausgeführt.

Ein Spy ist vorteilhaft, wenn nur ein Bruchteil des echten Verhaltens innerhalb eines Tests verändert werden muss. Dann kann der Rest des Verhaltens durch die echte Implementierung erzeugt werden. Da die Mockito Annotationen das Erstellen der Testkonfiguration sehr vereinfachen, werden Spies auch dann erzeugt, wenn nur eine echte Instanz aber kein Spy benötigt wird.

Der oben dargestellte Test-Code funktioniert wunderbar, solange es sich bei MySpiedService um eine Klasse und nicht um ein Interface handelt. Wird ein Interface mit @Spy verwendet, dann erzeugt Mockito keinen Spy sondern einen Mock. Das ist nicht besonders verwunderlich, weil Mockito nicht entscheiden kann, welche Implementierung an dieser Stelle instanziiert werden soll. Um das Problem zu umgehen bieten sich zwei Möglichkeiten an. Die Verwendung der Implementierung oder eine explizite Instanziierung.

@Spy // Implementierung 
private MySpiedServiceImpl mySpy;

@Spy // Explizite Instanziierung
private MySpiedService mySpy = new MySpiedServiceImpl();

Damit dieser Fehler sich nicht immer wieder in die eigenen Tests einschleicht, wäre eine automatisch Warnung schön. Mit dem Projekt Error Prone und einem eigenen BugChecker ist dies möglich.

Die folgende Klasse SpyOnInterface prüft während der Kompilierung alle Variablendefinitionen, in Form von VariablenTree Instanzen, ob sie Felder sind und die Annotation @Spy tragen.

@AutoService(BugChecker.class)
@BugPattern(name = "SpyOnInterface", summary = "A @Spy on an interface is only a mock", severity = SeverityLevel.WARNING)
public class SpyOnInterface extends BugChecker implements VariableTreeMatcher {
  private static final TypeExtractor<VariableTree> SPIED_VAR = fieldAnnotatedWithOneOf(Stream.of("org.mockito.Spy"));
  private static final String LINK_URL = "https://schegge.de/2021/08/mockito---der-spion-der-mich-hasste/";

  public static TypeExtractor<VariableTree> fieldAnnotatedWithOneOf(Stream<String> annotationClasses) {
    return extractType(allOf(isField(), anyOf(annotationClasses.map(Matchers::hasAnnotation).collect(toList()))));
  }

  public static <T extends Tree> TypeExtractor<T> extractType(Matcher<T> m) {
    return (tree, state) -> m.matches(tree, state) ? Optional.ofNullable(ASTHelpers.getType(tree)) : Optional.empty();
  }

  @Override
  public final Description matchVariable(VariableTree tree, VisitorState state) {
    return SPIED_VAR.extract(tree, state).map(type -> checkMockedType(type, tree, state)).orElse(NO_MATCH);
  }

  private Description checkMockedType(Type mockedClass, VariableTree tree, VisitorState state) {
    if (!mockedClass.isInterface() || tree.getInitializer() != null) {
      return NO_MATCH;
    }
    Description.Builder description = buildDescription(tree).setLinkUrl(LINK_URL);
    String modifiersWithAnnotations = requireNonNullElse(state.getSourceForNode(tree.getModifiers()), "");
    String modifiersWithoutSpy = modifiersWithAnnotations.replace("@org.mockito.Spy", "").replace("@Spy", "");
    String type = state.getSourceForNode(tree.getType());
    Name variable = tree.getName();
    description.addFix(SuggestedFix.replace(tree, format("@Mock %s %s %s;", modifiersWithoutSpy, type, variable)));
    return description.build();
  }
}

Alle gefundenen Variablendefinitionen werden in der Methode checkMockedType darauf geprüft, ob es sich hier um ein Interface handelt und die Variable keine Initialisierung besitzt. Als Ersetzungsvorschlag wird dem aktuelle Code @Mock vorangestellt und die bisherige @Spy Annotation in ihren beiden möglichen Varianten entfernt.

Fügt man den eigenen BugChecker in den Annotation-Processor Pfad ein, dann erzeugt der Compiler die folgende Warnung

[WARNING] ancestors/src/test/java/de/schegge/SpyOnInterfaceTest.java:[26,20] [SpyOnInterface] A @Spy on an interface is only a mock
    (see https://schegge.de/2021/08/mockito---der-spion-der-mich-hasste/)
  Did you mean '@Mock'?

Da MySpiedService ein Interface ist und keine Initialisierung erfolgt, wird an dieser Stelle eine @Mock Annotation vorgeschlagen. Dieser Hinweis und der Link auf den aktuellen Beitrag sollte ausreichende Hilfestellung für den Entwickler sein. Bei der Erstellung einer Patch Datei wird außerdem die folgende Änderung erzeugt.

--- ..\src\test\java\de\schegge\SpyOnInterfaceTest.java
+++ ..\src\test\java\de\schegge\SpyOnInterfaceTest.java
@@ -23,6 +23,6 @@
   private MyMockedService myMock;
 
-  @Spy
-  private MySpiedService mySpy;
+  @Mock 
+  private MySpiedService mySpy;
 
   @InjectMocks

Automatisierte Code Prüfungen, wie diese für fehlerhafte Verwendung der Mockito Annotationen, helfen Entwicklungsteams exotische aber wiederkehrende Fehler schnell zu finden und zu entfernen. Insbesondere wenn solche Fehlerquellen immer wieder in kollektive Vergessenheit geraten sind automatisierte Regelwerke ein Segen. Häufig ist es leider noch immer dem Glück geschuldet, dass ein Kollege sich an das Problem erinnert und eine Lösung präsentieren kann.