Mockito Framework

Die Herausforderung bei Unit Tests besteht darin, die zu testende Klasse isoliert zu testen, d.h. ohne Nebeneffekte von anderen Klassen und unabhängig von anderen Systemen. Mockito bietet dazu ein umfangreiches Framework an, welches zusammen mit JUnit sehr einfach und effizient eingesetzt werden kann. Ein Anwender berichtet.

Typisches Vorgehen beim Testen mit Mockito:

1. Die Abhängigkeiten werden gemockt bzw. im Test werden die Mocks verwendet.
2. Der oder die Test(s) wird / werden ausgeführt.
3. Die Ausführung wird überprüft.

Mockito im Projekt hinzufügen

Exemplarisch wird hier die Einbindung in Maven gezeigt:

<dependency>
   <groupId>org.mockito</groupId>
   <artifactId>mockito-core</artifactId>
   <version>2.22.0</version>
   <scope>test</scope>
</dependency>

Die zu testende Klasse

Die zu testende Klasse HealthCheck verwendet die beiden Services PersonService und MailService. Diese werden im Unit Test gemockt, damit die Klasse HealthCheck isoliert betrachtet werden kann:

public class HealthCheck {

  private final PersonService personService;
  private final MailService mailService;
  
  private List persons = new ArrayList<>();

  public HealthCheck(final PersonService personService, final MailService mailService) {
    this.personService = personService;
    this.mailService = mailService;
  }

  public int getBMI(final Person person) {
    final int mass = personService.getMass(person);
    final int size = personService.getSize(person);
    return mass * 10_000 / size / size;
  }

  public int getAvgMass() {
    int mass = 0;
    for (final Person person : persons) {
      mass += personService.getMass(person);
    }
    return mass / Math.max(persons.size(), 1);
  }
}

Die Methode getBMI() berechnet den Body Mass Index (BMI) einer Person. Dazu wird der Personen-Service aufgerufen, welcher Gewicht und Grösse der Person zurückgibt. Die Methode getAvgMass() berechnet zu statistischen Zwecken das Durchschnittsgewicht aller Personen. Wiederum wird der Personen-Service verwendet, um das Gewicht der einzelnen Person zu bestimmen.

Erste Schritte mit Mockito

Die Testklasse HealthCeckTest initialisiert die Services und das Testframework. Danach können die einzelnen Testfälle ausgeführt werden.

public class HealthCeckTest {

    @Mock
    private PersonService personService;

    @Mock
    private MailService mailService;

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @InjectMocks
    private HealthCheck healthCheck;

    @Before
    public void setUp() {
      when(personService.getMass(FRITSCHI)).thenReturn(64);
      when(personService.getSize(FRITSCHI)).thenReturn(152);
      when(personService.getMass(FROEHLICH)).thenReturn(58);
      when(personService.getSize(FROEHLICH)).thenReturn(162);
      when(personService.getMass(MAIER)).thenReturn(84);
      when(personService.getSize(MAIER)).thenReturn(182);
      when(personService.getMass(MUELLER)).thenReturn(66);
      when(personService.getSize(MUELLER)).thenReturn(180);
    }
// ...
}

Um die beiden Services, PersonService und MailService, als Mocks zu erstellen, werden sie mit der Annotation @Mock gekennzeichnet. Die Regel MockitoRule, annotiert mit @Rule, führt die Initialisierung von Mockito aus. Dabei werden die Mocks erstellt und die zu testenden Klasse HealthCheck unter Anwendung von Dependency Injection instanziiert. Alternativ zur Regel kann auch in der Methode setUp() die Initialisierung von Mockito durchgeführt werden:

    @Before
    public void setUp() {
      MockitoAnnotations.initMocks(this);
      // ...
    }

In der Methode setUp() wird das gewünschte Verhalten des Personen-Services definiert. Dies wird je nach Person, die der Methoden getMass() bzw. getSize() übergeben und deren Grösse bzw. Gewicht zurückgegeben wird, definiert.

Erster Test

    @Test
    public void testGetBMI() {
       assertEquals(27, healthCheck.getBMI(FRITSCHI));
       assertEquals(22, healthCheck.getBMI(FROEHLICH));
       assertEquals(25, healthCheck.getBMI(MAIER));
       assertEquals(20, healthCheck.getBMI(MUELLER));verify(personService, times(4)).getMass(any(Person.class));
       verify(personService, times(4)).getSize(any(Person.class));

Der eigentliche Test findet in der Methode testGetBMI() statt: Für die vier vordefinierten Personen wird der BMI berechnet und gegen das erwartete Resultat validiert. Schliesslich kann via Mockito geprüft werden, wie oft die beiden Service Methoden getMass() und getSize() aufgerufen worden sind. In unserem Beispiele je vier Mal.

Zweiter Test
In einem zweiten Test soll das Durchschnittsgewicht aller Personen bestimmt werden. Dazu werden im Test Setup die Liste der Personen dem Testobjekt mitgegeben:

    @Before
    public void setUp() {
      healthCheck.setPersons(getPersons());
      // ...
    }
    
    private List getPersons() {
      return List.of(MUELLER, FROEHLICH, MAIER, FRITSCHI);
    }

    @Test
    public void testGetAvgMass() {
      assertEquals(68, healthCheck.getAvgMass());
      verify(personService, never()).getSize(any(Person.class));
      verify(personService, atLeastOnce()).getMass(any(Person.class));
      verify(personService, atLeast(2)).getMass(any(Person.class));
      verify(personService, atMost(4)).getMass(any(Person.class));
      verify(personService, times(4)).getMass(any(Person.class));
    }

Das berechnete Durchschnittsgewicht wird mit dem erwarteten Wert verglichen. Weiter kann geprüft werden, wie oft die Service Methoden aufgerufen worden sind. Mockito stellt dabei verschiedene Prüfmethoden zur Verfügung:

  • Mockito.never() : nie aufgerufen
  • Mockito.atLeastOnce() : mindestens ein Mal aufgerufen
  • Mockito.atLeast(n) : mindestens n Mal aufgerufen
  • Mockito.atMost(n) : maximal n Mal aufgerufen
  • Mockito.times(n) : genau n Mal aufgerufen

Weitere Mockito Features

In einem nächsten Schritt werden weitere Features von Mockito, insbesondere ArgumentMatchers, ArgumentCaptor , Mockito.doThrow() und Mockito.doAnswer(), angeschaut.

Zuerst erweitern wir die Klasse HealthCheck um zwei weitere Methoden: Die Methode setCallDate() versendet allen Personen mit einem BMI >25 eine Mail zur Terminvereinbarung. Die Methode getCallDatePersons() sucht alle Personen, welche zu einem definierten Datum eine Terminvereinbarung gesetzt haben.

    public void setCallDate() {
      for (final Person person : persons) {
        if (getBMI(person) > 25) {
          final CallDate callDate = new CallDate(person, LocalDate.now());
          personService.setCallDate(callDate);
          mailService.sendCallDate(callDate.getPerson(), callDate.getDate());
        }
      }
    }

    public List getCallDatePersons(final LocalDate date) {
      return personService.getCallDatePersons().stream() //
          .filter(cd -> Objects.equals(date, cd.getDate())) //
          .map(cd -> cd.getPerson()) //
          .collect(Collectors.toList());
    }

Dritter Test
Die Klasse ArgumentCaptor erlaubt den Zugriff auf Methodenargumente während der Testausführung. Wir definieren ein Argument Captor für die Klasse CallDate, welche im Service Call setCallDate(CallDate callDate) verwendet wird:

    @Captor
    private ArgumentCaptor callDate;

Und die Testmethode:

    @Test
    public void testSetCallDate() {
      healthCheck.setCallDate();
      verify(personService, times(4)).getMass(any(Person.class));
      verify(personService, times(4)).getSize(any(Person.class));
     
      verify(personService, times(1)).setCallDate(callDate.capture());
      assertEquals(FRITSCHI, callDate.getValue().getPerson());
      assertNotNull(callDate.getValue().getDate());

      verify(mailService, times(1)).sendCallDate(eq(FRITSCHI), any(LocalDate.class));
    }

Der Aufruf der Methode und die Verifikation der Anzahl Service Calls haben wir bereits in den ersten Schritten mit Mockito kennengelernt. Neu in dieser Testmethode ist das Call Argument callDate, welches geprüft werden kann: Wir erwarten, dass der Service Call mit der Person
FRITSCHI ausgeführt worden ist. Das genaue Datum interessiert uns jedoch nicht, dieses wird lediglich auf NotNull geprüft.

Da der Service Call setCallDate() genau einmal ausgeführt worden ist, enthält das Call Argument auch nur einen Wert. Würde der Service Call mehrmals ausgeführt, könnten im Test auch mehrere Werte geprüft werden:

    callDate.getAllValues().forEach(cd -> {
      // ...
    });

Zudem wird in dieser Testmethode geprüft, ob und wie oft der Mail Service aufgerufen wurde. Dabei interessiert uns wiederum nur die Person, mit welcher der Service Call ausgeführt wurde. Das Datum kann beliebig sein. Rein intuitiv würde die Methode Mockito.verify() wie folgt aufgerufen:

    verify(mailService, times(1)).sendCallDate(FRITSCHI, any(LocalDate.class));

Dies führt nach kurzer Laufzeit jedoch zu einer Mockito Exception, weil hier konkrete Objekte ( FRITSCHI ) und Argument Matchers ( any() ) gemischt werden. D.h. entweder verwenden wir beim Verifizieren ausschliesslich konkrete Objekte oder aber ausschliesslich Argument Matchers wie Mockito.eq() und Mockito.any().

    verify(mailService, times(1)).sendCallDate(FRITSCHI, LocalDate.now());
    verify(mailService, times(1)).sendCallDate(eq(FRITSCHI), any(LocalDate.class));

Vierter Test
Im nächsten Beispiel fragen wir uns, wie die getestete Klasse auf eine Exception des Service reagiert.

    @Test(expected = IllegalStateException.class)
    public void testOneCallDatePersonsThrowsException() {
      doThrow(IllegalStateException.class).when(personService).getCallDatePersons();
      healthCheck.getCallDatePersons(LocalDate.now());
    }

Mit der Anweisung Mockito.doThrow() definieren wir, dass der Service Call getCallDatePersons() die angegebene Exception werfen soll. Da unsere getestete Klasse kein Exception Handling implementiert, gelangt die Exception zur Testmethode. Dies entspricht soweit dem erwarteten Verhalten. Deshalb wird die Testmethode zusätzlich mit der erwarteten Exception annotiert.

Fünfter Test
Im letzten Beispiel soll der Personen-Service eine Liste von Personen herausfiltern, mit welchen heute ein Termin vereinbart werden soll.

    @Test
    public void testGetCallDatePersons() {
      doAnswerOneCallDatePerson().when(personService).getCallDatePersons();
      final List persons = healthCheck.getCallDatePersons(LocalDate.now());
      assertEquals(1, persons.size());
      assertEquals(FRITSCHI, persons.get(0));
      verify(personService, times(1)).getCallDatePersons();
    }

    private Stubber doAnswerOneCallDatePerson() {
      return doAnswer(new Answer<List>() {

        @Override
        public List answer(final InvocationOnMock invocation) throws Throwable {
          return List.of(new CallDate(FRITSCHI, LocalDate.now()) //
          , new CallDate(FROEHLICH, LocalDate.now().minusDays(12)));
        }
      });
    }  

Mit der Anweisung Mockito.doAnswer() definieren wir, dass der Service Call getCallDatePersons() eine Liste von Personen zurückgibt, jedoch nur bei einer Person die Teminvereinbarung heute stattfinden soll. Interessant bei dieser Anweisung ist, dass allfällige Call Argumente als InvocationOnMock -Objekt mitgegeben und in der Methode answer() ausgewertet werden können. Damit ist ein dynamisches Verhalten zur Laufzeit möglich.

… und noch mehr Features

In diesem Artikel werden nur einige wesentliche Features von Mockito eingeführt, um den Einstieg in dieses weit verbreitete Framework zu vereinfachen. Es gibt jedoch noch viele Features, die hier nicht besprochen wurden und noch beliebig vertieft werden können. Z.B.:

• Mock vs. spy
• Call real method in mock
• Custom argument matchers

Einschränkungen von Mockito

Das Mockito Framework ist sehr mächtig und wurde in Version 2 deutlich erweitert. Trotzdem müssen zwei Limitationen erwähnt werden, welche bei der Evaluation und Verwendung dieses Testframeworks beachtet werden müssen:

1. Statische Methoden können nicht gemockt werden.
2. Private Methoden können nicht gemockt werden.

Als alternatives Testframework kann hier PowerMock verwendet werden.

Quellen und weitere Informationen

Mockito Home Page: https://site.mockito.org/
Unit Tests with Mockito: http://www.vogella.com/tutorials/Mockito/article.html
Usages of doThrow() doAnswer() doNothing() and doReturn() in mockito: https://stackoverflow.com/questions/28836778/usages-of-dothrow-doanswer-donothing-and-
doreturn-in-mockito Testbeispiele: https://gitlab.puzzle.ch/cga/java/mockito

Kommentare sind geschlossen.