Kontakt
stefan.bente[at]th-koeln.de
+49 2261 8196 6367
Discord Server
Prof. Bente Personal Zoom
Adresse
Steinmüllerallee 4
51643 Gummersbach
Gebäude LC4
Raum 1708 (Wegbeschreibung)
Sprechstunde nach Vereinbarung
Terminanfrage: calendly.com Wenn Sie dieses Tool nicht nutzen wollen, schicken Sie eine Mail und ich weise Ihnen einen Termin zu.

Mockito ist ein populäres Mocking-Framework für Java, das Integrationstests erheblich vereinfacht. Mit Mockito lassen sich Abhängigkeiten durch Mock-Objekte ersetzen, um einzelne Komponenten isoliert zu testen. Dies ermöglicht schnelle, gezielte Tests, die unabhängig von externen Systemen wie Datenbanken oder REST-APIs funktionieren.

Beispielcode-Repository
https://gitlab.com/archi-lab/public/testing-good-practices/-/tree/main/integration-testing-mockito
Keywords
Integrationstests, Mockito, Mocking, Mock, Test, Integration

Integration Testing mit Mockito

Wenn wir einen Spring-Service isoliert testen, wollen wir weder einen vollständigen Application-Context hochfahren noch eine echte Datenbank ansprechen. Beides ist langsam, nicht-deterministisch und prüft am Ende ohnehin etwas anderes als die Logik unseres Service. Mockito erlaubt uns, die Abhängigkeiten eines Service durch Mock-Objekte zu ersetzen, deren Verhalten wir vollständig kontrollieren. So testen wir das Verhalten des Service rein in Java - schnell, deterministisch und ohne jede Infrastruktur.

Diese Seite ist ein Katalog der wichtigsten Pattern, die beim Mockito-basierten Integrationstesten von Services immer wieder vorkommen. Die Code-Ausschnitte stammen aus dem Beispielprojekt, das diese Seite begleitet - einem kleinen Bahn-Ticket-System mit zwei Services und einer reinen Rechenregel:

  • TicketService.purchaseTicket(...) - legt einen Ticketkauf an. Hängt von einem Repository (TicketRepository) ab.
  • LoyaltyService.isFrequentTraveller(...) - entscheidet, ob ein Reisender im Vielfahrer-Programm ist.
  • FrequentTravellerRule - die reine Regel (“mindestens 500 € in den letzten sechs Monaten”). Keine Abhängigkeiten, keine Seiteneffekte - und damit kein Mockito-Fall, sondern ein normaler Unit-Test.

Unit-Test vs. Integrationstest

Die Abgrenzung zwischen beiden Testarten ist wichtig für das Verständnis, wofür man Mockito einsetzt. Ein Unit-Test prüft eine einzelne Klasse ohne Kollaborateure - typischerweise ein Aggregate Root, ein Domain Primitive oder eine reine Regelklasse wie FrequentTravellerRule. Dort brauchen Sie kein Mockito, Sie instanziieren die Klasse einfach.

Ein Integrationstest hingegen prüft das Zusammenspiel eines Service mit seinen Kollaborateuren - typischerweise dem Repository und benachbarten Services. Genau dieses Zusammenspiel isolieren wir mit Mockito, indem wir die Kollaborateure durch Mocks ersetzen und nur den einen (getesteten) Service echt laufen lassen.

Was wir mocken - und warum

Ein Application Service hängt typischerweise von Kollaborateuren ab, die mit der Außenwelt reden. Genau die ersetzen wir im Test durch Mocks:

Kollaborateur Warum wir ihn mocken
Repository (TicketRepository) Ersetzt die JPA-/Datenbankschicht. Wir wollen keine echte DB starten und auch nicht testen, ob JPA funktioniert - sondern ob unser Service das Repository korrekt benutzt.
benachbarter Service (TravellerService) Ersetzt ein anderes Aggregat. Dessen Logik hat eigene Tests; hier interessiert nur, dass und wie unser Service ihn aufruft.

Zwei Dinge bleiben echt (kein Mock):

  • der getestete Service selbst - wir bauen ihn mit seinen gemockten Abhängigkeiten zusammen und prüfen sein Verhalten;
  • reine Logik ohne Seiteneffekte wie FrequentTravellerRule - die wird nicht gemockt, sondern “as is” mitgenutzt und hat ihren eigenen Unit-Test.

Typische Patterns bei der Nutzung von Mockito

Pattern 1 - Mocks deklarieren

Es gibt zwei gängige Wege, Mocks zu erzeugen und in den Service zu injizieren. Beide sind korrekt - wählen Sie einen und bleiben Sie im Projekt konsistent dabei. In diesem Beispielprojekt habe ich mich dafür entschieden, die Mocks in der Setup-Methode manuell zu erstellen.

class TicketServiceTest {
    private TicketRepository ticketRepository;
    private TravellerService travellerService;
    private TicketService    ticketService;

    @BeforeEach
    void setUp() {
        ticketRepository  = mock( TicketRepository.class );
        travellerService  = mock( TravellerService.class );
        ticketService     = new TicketService( ticketRepository, travellerService );
    }
    
    // … die Tests …
}

Der Service wird hier über seinen Konstruktor mit den Mocks zusammengebaut (da sieht man dann auch, warum Constructor Injection besser ist als Field Injection). Der Vorteil: man sieht die Abhängigkeiten explizit, und der Test funktioniert ganz ohne Mockito-Annotationen.

Alternativ gibt es den annotationsgetriebenen Stil, der etwas kompakter ist, aber auch etwas “magischer”. So sieht dieselbe Testklasse mit Mockito-Annotationen aus:

@ExtendWith( MockitoExtension.class )
class TicketServiceTest {
    @Mock
    private TicketRepository ticketRepository;
    @Mock
    private TravellerService travellerService;
    @InjectMocks
    private TicketService ticketService;
    
    // … die Tests …
} 

Pattern 2 - Stubbing mit when().thenReturn()

Das Grundpattern: Wir sagen einem Mock, was er bei einem bestimmten Aufruf zurückgeben soll. Damit stellen wir die Welt her, in der unser Service-Aufruf laufen soll (das „given“).

@Test
void isFrequentTraveller_loadsTicketsViaServiceAndAppliesRule() {
    // given: der Ticket-Service liefert die Tickets des Reisenden
    TravellerId travellerId = new TravellerId( UUID.randomUUID() );
    when( ticketService.findTicketsOf( "johndoe" ) ).thenReturn( List.of(
            new Ticket( travellerId, new BigDecimal( "500.00" ), LocalDate.of( 2025, 12, 10 ) ) ) );

    // when
    boolean frequent = loyaltyService.isFrequentTraveller( "johndoe", REFERENCE );

    // then
    assertThat( frequent ).isTrue();
}

Lesen Sie die drei Abschnitte als given / when / then: erst den Mock einrichten, dann genau einen Aufruf des getesteten Service, dann die Überprüfungen.

Pattern 3 - thenAnswer(): das gespeicherte Objekt zurückgeben

save(...) eines Spring-Data-Repositories gibt die gespeicherte Entität zurück. Ein Mock gibt standardmäßig null zurück - das bringt jeden Service zum Absturz, der das Ergebnis von save weiterverwendet. Mit thenAnswer reichen wir das übergebene Argument einfach durch:

when( ticketRepository.save( any( Ticket.class ) ) )
        .thenAnswer( inv -> inv.getArgument( 0 ) );

Gelesen wird das so: “Was auch immer in save(...) hineingegeben wird, gib es unverändert zurück.” Das ist das übliche “Save-Echo”-Pattern und gehört in fast jeden Service-Test, der speichert und das Ergebnis zurückgibt - so wie TicketService.purchaseTicket(...), das das gespeicherte Ticket zurückliefert.

thenAnswer ist allgemeiner als thenReturn: Die Antwort wird bei jedem Aufruf aus den tatsächlichen Argumenten berechnet, statt einen vorab festgelegten Wert zu liefern.

Pattern 4 - Argument-Matcher (any, eq, anyString)

Oft ist uns das konkrete Argument egal (“irgendein Ticket”), oder wir kennen es im Test gar nicht (eine intern erzeugte ID). Dann verwenden wir Matcher statt konkreter Werte:

when( ticketRepository.save( any( Ticket.class ) ) ).thenAnswer( inv -> inv.getArgument( 0 ) );
when( travellerService.findByUsername( anyString() ) ).thenReturn( travellerId );

Hat eine Methode mehrere Parameter und soll nur eines exakt passen, der Rest egal sein, dann mischt man - aber nach einer festen Regel (Beispiel mit einem gedachten abgeleiteten Query):

when( ticketRepository.findByTravellerIdAndPurchaseDateAfter( eq( travellerId ), any() ) )
        .thenReturn( List.of() );

Es gilt: Sobald ein Argument ein Matcher ist, müssen alle Argumente Matcher sein. Einen konkreten Wert, der exakt passen soll, schreibt man dann als eq(wert) - nicht als nackten Wert. Ein Mix wie find( travellerId, any() ) (erstes Argument “nackt”, zweites Argument Matcher) wirft zur Laufzeit eine InvalidUseOfMatchersException.

Die wichtigsten Matcher sind:

  • any() (irgendetwas, inkl. null)
  • any(Ticket.class) (irgendein Ticket, nicht null)
  • eq(x) (genau x)
  • anyString() (irgendein String, nicht null)
  • anyInt() (irgendein int, nicht null)

Pattern 5 - Interaktionen prüfen mit verify()

when richtet den Mock ein; verify prüft hinterher, dass (und wie oft) ein Mock aufgerufen wurde. Das ist der Kern eines Integrationstests: nicht nur das Ergebnis zählt, sondern auch, ob der Service korrekt mit seinen Nachbarn gesprochen hat.

// genau einmal gespeichert
verify( ticketRepository ).save( ticket );

// der Service wurde mit dem richtigen Username konsultiert
verify( travellerService ).findByUsername( "johndoe" );

// im Fehlerfall: sicherstellen, dass gar nicht gespeichert wurde
verify( ticketRepository, never() ).save( any() );

verify(mock) ohne Anzahl bedeutet dasselbe wie times(1), also genau ein Mal. Weitere nützliche Modi sind:

  • never() (kein Aufruf)
  • times(n) (macht die Aufrufzahl zum Teil des Vertrags - nützlich, wenn ein Pfad z. B. zweimal speichern muss und ein anderer nur einmal)
  • atLeastOnce() (der Aufruf muss mindestens einmal erfolgen, aber es könnte auch öfter sein)
  • atLeast(n) (der Aufruf muss mindestens n-mal erfolgen, aber es könnte auch öfter sein)
  • atMost(n) (der Aufruf darf höchstens n-mal erfolgen, aber es könnte auch weniger sein)

Pattern 6 - verifyNoInteractions(): kein Schreibvorgang, wenn etwas schiefgeht

Eine der wichtigsten Prüfungen ist es, dass der Service nichts weiter tut (insbesondere nicht das Repository zum Speichern aufruft), wenn ein Fehler auftritt - z. B. wenn der angegebene Username nicht existiert.

@Test
void purchaseTicket_unknownUsername_throwsAndNeverTouchesRepository() {
    when( travellerService.findByUsername( "unknown" ) )
            .thenThrow( new TravellerNotFoundException( "unknown" ) );

    assertThatThrownBy( () ->
            ticketService.purchaseTicket( "unknown", new BigDecimal( "49.99" ), LocalDate.of( 2026, 1, 15 ) ) )
            .isInstanceOf( TravellerNotFoundException.class );

    verifyNoInteractions( ticketRepository );
}

verifyNoInteractions(mock) (für gar keinen Aufruf eines Mocks) bzw. never().save(...) stellt sicher, dass der Service nicht teilweise ausgeführt wird - es kann kein halb-angelegter Ticketkauf in der Datenbank landen, wenn der Reisende gar nicht existiert. Das ist die Test-Entsprechung der Idee, dass ein Aggregat seine Invarianten konsistent hält.

Pattern 7 - Fehlerpfade des Kollaborateurs mit thenThrow()

Ein Kollaborateur kann fehlschlagen. Wir simulieren das, indem der Mock eine Exception wirft, und prüfen, dass unser Service korrekt reagiert (durchreicht, übersetzt oder abbricht).

@Test
void purchaseTicket_unknownUsername_throwsAndNeverTouchesRepository() {
    when( travellerService.findByUsername( "unknown" ) )
            .thenThrow( new TravellerNotFoundException( "unknown" ) );

    assertThatThrownBy( () ->
            ticketService.purchaseTicket( "unknown", new BigDecimal( "49.99" ), LocalDate.of( 2026, 1, 15 ) ) )
            .isInstanceOf( TravellerNotFoundException.class );
}

Wichtig ist, dass Sie auf den fachlichen Exception-Typ prüfen, nicht auf RuntimeException. Würden wir die generische RuntimeException erwarten, käme auch eine versehentliche NullPointerException als “korrekt abgelehnt” durch. Der Test wäre dann unehrlich darüber, warum der Aufruf gescheitert ist. Das Festnageln auf den konkreten Typ (TravellerNotFoundException) hält den Ablehnungstest “ehrlich”.

Pattern 8 - ArgumentCaptor: was genau wurde gespeichert?

Manchmal reicht “save wurde aufgerufen” nicht - wir wollen wissen, womit. Der Service erzeugt die Entität ja intern (purchaseTicket baut das Ticket selbst); im Test haben wir keine direkte Referenz darauf. Der ArgumentCaptor fängt das tatsächlich übergebene Objekt ab, damit wir es prüfen können:

@Test
void purchaseTicket_savesTicketWithPriceDateAndTraveller() {
    LocalDate date = LocalDate.of( 2026, 1, 15 );
    when( travellerService.findByUsername( "johndoe" ) ).thenReturn( travellerId );
    when( ticketRepository.save( any( Ticket.class ) ) ).thenAnswer( inv -> inv.getArgument( 0 ) );

    ticketService.purchaseTicket( "johndoe", new BigDecimal( "49.99" ), date );

    ArgumentCaptor<Ticket> captor = ArgumentCaptor.forClass( Ticket.class );
    verify( ticketRepository ).save( captor.capture() );
    assertThat( captor.getValue().getTravellerId() ).isEqualTo( travellerId );
    assertThat( captor.getValue().getPrice() ).isEqualByComparingTo( "49.99" );
    assertThat( captor.getValue().getPurchaseDate() ).isEqualTo( date );
}

Als Faustregel gilt: ArgumentCaptor verwendet man, wenn das interessierende Objekt im Service entsteht und Sie seinen Inhalt prüfen wollen. Geht es nur um die Identität eines Objekts, das Sie bereits in der Hand haben, genügt verify( repo ).save( dasObjekt ).

Pattern 9 - Optional als Rückgabe: gefunden / nicht gefunden

Spring-Data-findBy…-Methoden geben oft Optional zurück (z. B. TravellerRepository.findByUsername). Über den Inhalt dieses Optional steuern wir im Test die beiden Welten “Entität existiert” und “existiert nicht” - und damit fast alle Verzweigungen eines Services:

// gefunden  → Service arbeitet weiter
when( travellerRepository.findByUsername( "johndoe" ) )
        .thenReturn( Optional.of( new Traveller( "johndoe" ) ) );

// nicht gefunden → Service wirft Exception
when( travellerRepository.findByUsername( "ghost" ) )
        .thenReturn( Optional.empty() );
assertThatThrownBy( () -> travellerLookup.findByUsername( "ghost" ) )
        .isInstanceOf( TravellerNotFoundException.class );

Mit demselben Mock und nur unterschiedlichem Optional-Inhalt decken Sie systematisch Happy Path und “nicht gefunden” ab. Das ist das typische Pattern hinter einem findBy…(…).orElseThrow(…) im Service.

Was Sie nicht mocken sollten

Mocking ist ein Werkzeug für Kollaborateure mit Außenwirkung (Repositories, benachbarte Services). Nicht alles gehört gemockt:

  • Keine reine Logik mocken (FrequentTravellerRule). Sie ist klein, deterministisch und ohne Seiteneffekte - nutzen Sie sie echt (per @Spy oder einfach new …()) und testen Sie sie separat. Ein gemocktes Regelobjekt würde genau die Rechenlogik verbergen, die Sie prüfen wollen.
  • Keine Aggregate/Entities mocken, deren Verhalten zum Testgegenstand gehört. Oben sind Ticket und Traveller jeweils echt, nur ihr Repository ist gemockt. So testen Sie die echte Logik der Entität mit.
  • Keine Value Objects / IDs mocken (TravellerId, TicketId). Die baut man einfach.
  • Nichts mocken, was Ihnen nicht gehört (JDK-Typen, fremde Bibliotheksklassen).

Faustregel: Mocken Sie eine Grenze (Datenbank, Nachbar-Aggregate), nicht den Kern dessen, was der Test eigentlich prüfen soll.