Start / Cheat Sheets

Cheat Sheet

Mockito & Testcontainers — Cheat Sheet

Stand: · Java Testing mit JUnit, Mockito und Testcontainers

MockitoTestcontainersSpring BootIntegrationstests

Der Spickzettel zu Attrappen und echter Infrastruktur aus dem Seminar Java Testing mit JUnit, Mockito und Testcontainers. Das Testhandwerk selbst — Assertions, parametrisierte Tests, Lifecycle — steht auf dem zweiten Blatt: JUnit 6. Maßgeblich sind die Mockito-Dokumentation und die Testcontainers-Dokumentation; hier steht die Auswahl, die im Alltag tatsächlich gebraucht wird.

Test Doubles unterscheiden

DoubleLiefertPasst für
DummynichtsPflichtparameter ohne Bedeutung
Stubfeste Antwortenein Lesezugriff im Test
Fakeechtes Verhaltenwiederkehrende Nutzung über viele Tests
Spyechtes Verhalten plus ProtokollAltcode ohne saubere Schnittstelle
MockAntworten plus PrüfungAufruf ist selbst das erwartete Ergebnis

Wer nicht sagen kann, welche der fünf Zeilen gemeint ist, braucht die Attrappe wahrscheinlich nicht. Und ein Stub, der verifiziert wird, ist still zum Mock geworden — der Test prüft dann plötzlich Interna.

Modul: Test Doubles und Mockito-Grundlagen

Mockito-Annotationen

AnnotationBedeutung
@Mockeine Attrappe, alle Methoden liefern Standardwerte
@Spyein echtes Objekt, dessen Aufrufe protokolliert werden
@Captorein vorbereiteter ArgumentCaptor für einen Typ
@InjectMocksder Prüfling, in den die Mocks eingesetzt werden
@ExtendWith(MockitoExtension.class)
class BestellServiceTest {
  @Mock ZahlwerkClient zahlwerk;
  @Mock LagerBestand lager;
  @InjectMocks BestellService service;
}

Ohne die Extension bleiben alle vier null — dann fehlt entweder @ExtendWith oder das Artefakt mockito-junit-jupiter. Ein nicht gestubbter Aufruf liefert null, 0 oder eine leere Sammlung; der Test läuft weiter und scheitert drei Zeilen später an einer NullPointerException.

Stubbing

when(zahlwerk.belaste(any(), eq(euro("42.90"))))
    .thenReturn(Zahlung.erfolgreich("KG-4711"));

when(zahlwerk.status("KG-4711")).thenReturn(OFFEN).thenReturn(GEBUCHT);

doThrow(new LagerLeer()).when(lager).reserviere(HONIG);

Zwei thenReturn hintereinander beantworten den ersten und den zweiten Aufruf verschieden. Argument Matcher sind alles oder nichts: Sobald ein Argument ein Matcher ist, müssen es alle sein — ein fester Wert wird dann mit eq umschlossen. Je genauer der Matcher, desto genauer sagt der rote Test, was sich geändert hat.

In BDD-Schreibweise heißt dasselbe given(…).willReturn(…) und then(mock).should().… — gleiche Wirkung, andere Lesart. Im Team eine Schreibweise wählen und dabei bleiben.

FallMittel
Antwort hängt vom Argument abthenAnswer mit getArgument
Generischer RückgabetypthenAnswer statt thenReturn mit Cast
Fertiges asynchrones ErgebniscompletedFuture im thenReturn
Nur einzelne Methoden ersetzenSpy plus doReturn

Bei einem Spy immer doReturn statt when — sonst läuft die echte Methode schon beim Stubben. Ein Answer, der länger als drei Zeilen wird, ist meist ein Fake, der noch keine Klasse hat. Asynchrones braucht keine Wartezeit im Test: ein abgeschlossenes CompletableFuture zurückgeben, Callbacks im Answer direkt aufrufen.

Module: Test Doubles und Mockito-Grundlagen · Fortgeschrittenes Stubbing

Verifikation

ModusBedeutung
ohne Angabegenau ein Aufruf
times(n)genau n Aufrufe
never()kein Aufruf
atLeast(n), atMost(n)mindestens, höchstens n Aufrufe
only()dieser Aufruf und kein anderer auf dem Mock
verify(zahlwerk).belaste(KUNDE, euro("42.90"));
verify(zahlwerk, never()).storniere(any());

var reihe = inOrder(lager, zahlwerk);
reihe.verify(lager).reserviere(APFELKISTE);
reihe.verify(zahlwerk).belaste(any(), any());

times(1) ist nur die ausgeschriebene Form des Standardfalls. Die Reihenfolge nur prüfen, wenn sie eine Regel ist — hier ist sie eine: Reserviert wird vor der Belastung, sonst zahlt der Kunde für eine Kiste, die es nicht gibt.

Für Argumente, die erst im Prüfling entstehen:

SituationMittel
Argument ist ein fester, bekannter Wertdirekt im verify angeben
Argument entsteht erst im PrüflingArgumentCaptor oder assertArg
Mehrere Aufrufe, alle Argumente prüfenArgumentCaptor mit getAllValues
Bedingung wiederholt sich in vielen Testseigener Matcher mit argThat
verify(zahlwerk).belaste(assertArg(a ->
    assertThat(a.referenz()).startsWith("KG-")));

assertArg (seit Mockito 5.3) prüft im Matcher und spart das Einfangen; bei mehreren Aufrufen bleibt der Captor besser, denn getValue liefert nur den letzten, getAllValues alle in Aufrufreihenfolge.

Maß halten: Verifiziert wird, was der Nutzer merken würde, wenn es ausbliebe.

AufrufVerifizieren?
Belastung bei Zahlwerk ausgelöstja, das ist der Nebeneffekt
Preis beim Rechner abgefragtnein, das Ergebnis zeigt es
Log-Ausgabe geschriebennein, das ist kein fachliches Verhalten
Reservierung vor Belastungja, wenn die Reihenfolge eine Regel ist

Modul: Interaktionen und Zusammenarbeit prüfen

Statisches, Finales und Strictness

try (MockedStatic<Uhr> mock = mockStatic(Uhr.class)) {
  mock.when(Uhr::jetzt).thenReturn(MITTAG);
  assertThat(lieferfenster.offen()).isTrue();
}

Der Mock gilt nur im Block und nur im aktuellen Thread. Seit Java 21 dürfen Bibliotheken keinen Java-Agenten mehr an die eigene JVM hängen: Der Inline Mock Maker braucht Mockito als -javaagent auf der Kommandozeile, sonst warnt die JVM bei jedem Testlauf. Auf Java 25 gehört der Eintrag zur Grundeinrichtung des Builds — und in die Pipeline, nicht nur auf den Entwicklerrechner.

Wichtiger als das Wie ist meist das Warum:

Wird gemocktMeist die bessere Antwort
statische Uhr oder Zufallsquelleals Abhängigkeit hineinreichen
Konstruktoraufruf im PrüflingFabrik oder Konstruktorparameter
finale Klasse aus der Fremdbibliothekeigene Schnittstelle davor legen
Singleton oder Enum mit VerhaltenVerhalten in ein Objekt herausziehen

Alle vier gehen mit Mockito. Alle vier sind zugleich ein Befund über die Produktionsarchitektur.

Strenge Stubs sind die Voreinstellung und sollten es bleiben: Ein ungenutzter Stub heißt meist, dass der Test etwas anderes prüft als gedacht; ein Argument-Mismatch, dass der Prüfling anders aufruft als angenommen. LENIENT für eine ganze Klasse ist ein Schuldschein, lenient() für einen einzelnen Stub eine Entscheidung.

Modul: Fortgeschrittenes Stubbing und schwierige Abhängigkeiten

Wenn das Mocking wehtut

Was im Test auffälltWas in der Anwendung fehlt
Setup länger als der Testein Schnitt, der Verantwortung trennt
Mock liefert Mock liefert Mockeine Schnittstelle statt einer Kette
Reihenfolge muss geprüft werdenein Ergebnis, das die Regel sichtbar macht
Jeder Umbau färbt zwanzig Tests rotTests am Verhalten statt an der Struktur

Der Test ist das erste Werkzeug, das die Architektur benutzt — und das erste, das sich beschwert. Die drei häufigsten Unberechenbaren kommen über den Konstruktor herein, dann braucht kein Test dafür einen statischen Mock:

QuelleIn ProduktionIm Test
ZeitSystemuhr als Clockfeste Uhr im Konstruktor
ZufallZufallsquelle als Portfeste Folge von Werten
Identitäterzeugte UUID als Portmitzählender Generator

Ein Test Data Builder hält im Test nur das Abweichende sichtbar — eineBestellung().mit(APFELKISTE, 10).fuerKunde(STAMMKUNDE).build(). Alles, was dort nicht steht, ist für die geprüfte Regel egal; genau das ist die Aussage.

Modul: Gute Testarchitektur statt Mocking-Überlastung

Testcontainers 2: Umstieg und Einrichtung

Was der Realismus kostet — und was er dafür findet:

Unit-TestContainer-Test
StartkeinerSekunden je Image
Laufzeit je TestMillisekundenZehntel bis Sekunden
VoraussetzungJVMContainer-Laufzeit
FindetRechenfehlerSchema, Dialekt, Mapping

Die Startzeit fällt einmal je Testklasse an, nicht je Test — das entscheidet über die Gesamtlaufzeit. Container-Tests ergänzen Unit-Tests, sie ersetzen sie nicht.

ThemaTestcontainers 1.xTestcontainers 2.x
Artefaktpostgresqltestcontainers-postgresql
Paketorg.testcontainers.containersorg.testcontainers.postgresql
JUnit 4als Rule unterstütztentfallen
Jupiter-Moduljunit-jupitertestcontainers-junit-jupiter

Die Gruppe bleibt org.testcontainers — geändert haben sich Artefaktname und Paket. Über die BOM (testcontainers-bom, aktuell 2.0.5) kommen testcontainers-junit-jupiter und das Technologiemodul ohne eigene Version dazu.

@Container
static GenericContainer<?> cache =
    new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379)
        .waitingFor(Wait.forLogMessage(".*Ready to accept connections.*", 1))
        .withStartupTimeout(Duration.ofSeconds(90));

var port = cache.getMappedPort(6379);

Der Container-Port wird nie fest zugeordnet — getMappedPort liefert den zufällig vergebenen Hostport. Das Feld muss statisch sein, sonst startet jeder Test die Infrastruktur erneut, und die Klasse braucht @Testcontainers.

StrategiePasst für
Wait.forListeningPortVoreinstellung, Dienst öffnet einen Port
Wait.forLogMessageDienst meldet seine Bereitschaft im Log
Wait.forHttpDienst hat einen Health-Endpunkt
Wait.forHealthcheckImage bringt einen eigenen Healthcheck mit
Wait.forSuccessfulCommandBereitschaft ist nur im Container prüfbar

Ohne Angabe wartet Testcontainers bis zu 60 Sekunden auf den ersten veröffentlichten Port. Startet ein Container nicht, steht die Ursache im Container-Log — getLogs nachträglich, ein Log-Consumer live; der Test allein meldet nur eine abgelaufene Wartezeit.

Modul: Testcontainers 2 einrichten und verwenden

Lebenszyklus der Container

LebenszyklusStartetPasst für
Feld je Testmethodevor jedem Testmaximale Isolation
Statisches Feldje Testklasseder Regelfall
Singletoneinmal je Testlaufviele Klassen, eine Datenbank
Wiederverwendungüberlebt den Lauflokale Arbeit, nicht CI

Wiederverwendbare Container gelten weiterhin als experimentell und werden für CI ausdrücklich nicht empfohlen — ist die Wiederverwendung lokal aktiv und in der Pipeline nicht, verhalten sich die Tests unterschiedlich.

Datenbank- und Spring-Boot-Tests

@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class BestellungIT {
  @Container
  @ServiceConnection
  static PostgreSQLContainer<?> db =
      new PostgreSQLContainer<>("postgres:17-alpine");
}

Ohne @ServiceConnection müsste jede Eigenschaft von Hand über @DynamicPropertySource gesetzt werden. Das Image bekommt ein festes Tag — mit latest wechselt die Datenbank still die Hauptversion.

AnnotationLädt
@DataJpaTestnur die Persistenzschicht, mit Rollback
@WebMvcTestnur die Weboberfläche, ohne Datenbank
@SpringBootTestdie ganze Anwendung, ohne Server
@SpringBootTest mit RANDOM_PORTdie ganze Anwendung samt echtem Server

Je größer der Ausschnitt, desto langsamer der Test und desto unschärfer die Fehlermeldung. Beim Test über echtes HTTP läuft der Aufruf in einem anderen Thread als der Test — kein Rollback, geschriebene Daten bleiben liegen. Für einzelne Beans im Kontext gibt es @MockitoBean und @MockitoSpyBean.

WegVorteilPreis
Rollback je Testschnell, kein Aufräumcodeprüft kein echtes Commit
Tabellen leerenprüft echtes VerhaltenReihenfolge der Constraints
Container je Klassevollständig isoliertStartzeit je Testklasse
Quelle des BestandsEignung
Migration alleinleeres Schema, Test legt alles selbst an
Testdaten-Skript nach der Migrationgemeinsamer Grundbestand für viele Tests
Test Data Builder im Testfachlich lesbar, je Test genau das Nötige
Datenbank-Abzug aus Produktionungeeignet, personenbezogen und instabil

Migrationen gehören in den Test: Flyway oder Liquibase laufen gegen den frischen Container, damit eine nicht abwärtskompatible Migration im Pull Request auffällt statt beim Ausrollen. Der beste Bestand ist der kleinste, aus dem sich die geprüfte Regel noch ableiten lässt.

Modul: Datenbank- und Spring-Boot-Integrationstests

Verbund, Pipeline und Coverage

static Network netz = Network.newNetwork();

@Container
static GenericContainer<?> zahlwerk =
    new GenericContainer<>("wiremock/wiremock:3.9.1")
        .withNetwork(netz)
        .withNetworkAliases("zahlwerk")
        .withExposedPorts(8080);

Innerhalb des Netzwerks lautet die Adresse zahlwerk:8080 — von außen gilt der gemappte Port; über localhost finden sich die Container nicht.

Einzelne ContainerComposeContainer
Definitionim Java-Codein einer Compose-Datei
Wartebedingungje Container steuerbarüber die Datei
Wiederverwendungim Testauch für die lokale Entwicklung
Passt fürzwei bis vier Diensteeine vorhandene Compose-Umgebung
JaCoCo-ZählerBedeutung
Instruktionenausgeführte Bytecode-Anweisungen
ZeilenQuellzeilen mit mindestens einer Ausführung
Branchesgenommene Zweige von if und switch
Komplexitätabgedeckte Pfade gegenüber allen Pfaden

Branch-Abdeckung sagt mehr als Zeilenabdeckung — eine Zeile mit if wird von einem Testfall halb geprüft. Und Coverage beantwortet ohnehin nur die halbe Frage:

CoverageMutation Testing
Wurde die Zeile ausgeführtFällt der Test bei falschem Code auf
Schnell, in jedem BuildLangsam, nachts oder je Modul
Zeigt LückenZeigt wirkungslose Tests
Lässt sich leicht schönenLässt sich kaum täuschen

Flaky Tests sind eine Aussage, kein Rauschen. Ein sporadisch roter Test hat eine Ursache: Zeit, Reihenfolge, Nebenläufigkeit oder Netz. Wer ihn wiederholt, bis er grün ist, hat die Ursache nach Produktion verschoben.

Ein KI-Assistent findet Grenzfälle, die beim Schreiben übersehen wurden — er kennt aber die fachliche Regel nicht und leitet die Erwartung aus dem Code ab. Ein generierter Test, der das aktuelle Verhalten festschreibt, zementiert auch den Fehler. Übernommen wird ein Vorschlag erst, wenn jemand seine fachliche Aussage benennen kann.

Modul: Komplexe Testumgebungen, Pipelines und KI-Unterstützung

Typische Fallen

  • Jede Abhängigkeit reflexhaft mocken, auch die Klasse mit reiner Rechenlogik.
  • Die ersetzte Abhängigkeit nirgends echt testen — und niemand merkt es.
  • @InjectMocks bei mehreren passenden Konstruktoren still den falschen wählen lassen.
  • Festen Wert und Matcher mischen — die Meldung nennt eine ganz andere Ursache.
  • Stubs ins gemeinsame Setup legen, wo die meisten Tests sie nicht brauchen.
  • any überall setzen und den Test grün lassen, obwohl sich Argumente änderten.
  • when auf einem Spy anwenden und dabei die echte Methode ausführen.
  • verifyNoMoreInteractions in jedem Test — jede Änderung wird rot.
  • Den Captor vor dem Aufruf auswerten, wo er noch leer ist.
  • MockedStatic nicht schließen und alle folgenden Tests des Threads färben.
  • Deep Stubs benutzen: kurze Kette, unsichtbare Abhängigkeit.
  • Den alten 1.x-Paketnamen im Import stehen lassen — die Klasse wird nicht gefunden.
  • Feste Ports im Test verwenden statt getMappedPort.
  • Image ohne Tag angeben, dann ändert sich der Testlauf ohne Commit.
  • @SpringBootTest auf jede Testklasse setzen und zehn Minuten Laufzeit kaufen.
  • Je Testklasse andere Properties setzen und die Kontextwiederverwendung verhindern.
  • Eine Coverage-Quote erzwingen — es entstehen Tests ohne Assertion.

Zum Seminar Java Testing mit JUnit, Mockito und Testcontainers