1. Argumente prüfen: „aufgerufen" ≠ „richtig"
In Mockito-Tests reicht der bloße Fakt eines Aufrufs normalerweise nicht aus. Im echten Backend — und auch im ContentHub — ist entscheidend, welche Daten wir nach außen senden. Die Moderation kann man auch mit einem leeren Text aufrufen, oder mit dem Text eines falschen Artikels, oder ohne title — formal „der Aufruf fand statt", aber das Systemverhalten ist schon ein anderes.
Hier ist es sinnvoll, den Fokus auf den ausgehenden Payload zu verengen. Deshalb wird ModerationClient unten bewusst in einer engeren Form gezeigt: Er nimmt ein DTO und gibt nichts zurück. Uns ist jetzt wichtiger zu sehen, welche Daten nach außen fliegen, als an einer fixen Service-Signatur festzuhalten.
Stellen wir uns ein vereinfachtes Stück aus unserer Domäne vor. Es gibt eine Integration ModerationClient, die eine Textprüfungs-Anfrage annimmt. Angenommen, wir senden nicht nur einen String, sondern ein kleines DTO. In Java hält man das praktischerweise als record, um keine Menge Getter schreiben zu müssen.
interface ModerationClient {
// Externe Abhängigkeit: hier "verlassen" wir unseren Service
void moderate(ModerationRequest request);
}
// DTO, das in das externe System fliegt
record ModerationRequest(long articleId, String text) { }
Und der Service, der die Anfrage zusammenbauen. Hier ist absichtlich das „typische Leben" eingebaut: Wir rufen trim() auf, weil man sonst Text mit Leerzeichen senden und denken könnte, er sei „nicht leer".
class ArticleWorkflowService {
// Abhängigkeit, die wir im Test mocken werden
private final ModerationClient moderationClient;
ArticleWorkflowService(ModerationClient moderationClient) {
this.moderationClient = moderationClient;
}
void submit(long articleId, String text) {
// Wichtiges Geschäftsdetail: Leerzeichen vor dem Senden bereinigen
moderationClient.moderate(new ModerationRequest(articleId, text.trim()));
}
}
Jetzt schauen wir uns einen Test an, der angeblich „den Aufruf prüft", aber der Bedeutung nach fast nichts beweist:
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class ArticleWorkflowServiceTest {
@Test
void submits_text_for_moderation_but_weak() {
// Arrange: Mock und Service vorbereiten
ModerationClient client = mock(ModerationClient.class);
ArticleWorkflowService service = new ArticleWorkflowService(client);
// Act: Aktion ausführen
service.submit(42L, " hello ");
// Assert: prüft "irgendwas", aber zu schwach
verify(client).moderate(any()); // Test ist "mit allem einverstanden"
}
}
Der Test ist grün, aber er ist auch grün in einer Welt, wo ArticleWorkflowService articleId = 0 sendet, oder gar kein trim() macht, oder den Text " " (nur Leerzeichen) schickt. Das heißt: „Ein Aufruf fand statt" haben wir bewiesen, aber „Der Aufruf war sinnvoll" — nicht.
Genau deshalb reicht ein einfaches verify(...).someMethod(...) oft nicht: Man möchte nicht nur den Fakt des Aufrufs prüfen, sondern den Sinn der übergebenen Daten. Dafür nützlich sind drei Werkzeuge: BDD-Stil (BDDMockito), Argument-Matchers und ArgumentCaptor.
2. BDDMockito: dasselbe Mockito, nur in erzählerischer Form
BDDMockito ist keine andere Bibliothek und kein anderes Mockito. Es ist dasselbe Werkzeug, nur mit einer Syntax, die sich wie eine Mini-Geschichte liest: given (gegeben), when (wenn), then (dann). Besonders bei Interaktionstests ist das praktisch: Sie ähneln weniger einem „Aufrufskript" und lesen sich besser wie eine Geschichte.
Um den Unterschied zu sehen, ist ein Vergleich der „klassischen" und der BDD-Variante in einer Tabelle hilfreich:
| Was wir tun | Klassisches Mockito | BDDMockito |
|---|---|---|
| Antwort der Abhängigkeit konfigurieren | when(x.m()).thenReturn(v) | given(x.m()).willReturn(v) |
| Ausnahme konfigurieren | when(x.m()).thenThrow(e) | given(x.m()).willThrow(e) |
| Aufruf prüfen | verify(x).m() | then(x).should().m() |
| Anzahl Aufrufe prüfen | verify(x, times(2)).m() | then(x).should(times(2)).m() |
Aus Sicht der JVM und von Mockito unter der Haube ändert sich nichts. Nur die „Sprache" ändert sich, in der man den Test erzählt.
So kann eine BDD-Prüfung aussehen, dass der Text vor dem Senden zur Moderation wirklich getrimmt wird:
import org.junit.jupiter.api.Test;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
class ArticleWorkflowServiceBddTest {
@Test
void trims_text_before_sending_to_moderation() {
// Arrange
ModerationClient client = mock(ModerationClient.class);
ArticleWorkflowService service = new ArticleWorkflowService(client);
// Act
service.submit(42L, " hello ");
// Assert: prüfen, dass genau "hello" geflogen ist, nicht " hello "
then(client).should().moderate(new ModerationRequest(42L, "hello"));
}
}
Beachte eine kleine, aber angenehme Sache: then(client).should() trennt die Interaktionsprüfung gut visuell vom Rest des Tests. Für Anfänger reduziert das oft die Verwirrung: Wo konfigurieren wir das Verhalten, wo rufen wir die Methode auf, und wo prüfen wir das Ergebnis.
Noch ein praktischer Vorteil des BDD-Stils: Er verträgt sich gut mit der gewohnten Struktur Arrange – Act – Assert, macht die Blöcke aber auf Sprachebene etwas expliziter. Im Arrange verwenden wir normalerweise given(...), im Assert — then(...).should(...). Der Test beginnt wie eine Geschichte auszusehen, nicht wie ein Ritual mit Beschwörungsformeln.
3. Argument-Matchers: Präzision ohne Mikro-Management
Hier schaltet sich normalerweise der „Modus des ehrlichen Perfektionisten" ein. Einerseits möchte man Argumente genau prüfen, damit der Test wirklich Defekte erkennt. Andererseits werden Tests, wenn man alles über vollständige Gleichheit prüft, schnell zerbrechlich: Jedes kosmetische Refactoring eines Strings, einer Textformatierung oder eines zusätzlichen Felds beginnt, die halbe Test-Suite zu brechen. Argument-Matchers helfen, die Balance zu halten: Man sagt „dieser Wert ist kritisch, aber hier ist mir nur das inhaltliche Merkmal wichtig."
Beginnen wir mit einer kleinen Situation aus ContentHub. Angenommen, bei der Veröffentlichung eines Artikels senden wir eine Benachrichtigung an den PublicationNotificationSender. Aus Testsicht ist manchmal wichtig, dass die Benachrichtigung genau für articleId = 42 abging, aber der genaue slug kann sich wegen kleiner Normalisierungsregeln ändern (Bindestriche, Punkte, Zusatzsuffixe). Dann ist es nützlich zu prüfen: id — genau, slug — zumindest nach einem Muster.
interface PublicationNotificationSender {
// Integration/Port: hierhin fliegt die Veröffentlichungsbenachrichtigung
void sendPublished(long articleId, String slug);
}
Die nützlichsten Matchers: Mini-Spickzettel
Um die Vorlesung nicht zum Mockito-Handbuch zu machen, sammeln wir die nützlichsten Matchers in einer kleinen Tabelle — genau die, die in normalen Backend-Tests wirklich vorkommen:
| Matcher | Was er fixiert | Wann nützlich |
|---|---|---|
| eq(value) | Exakter Wert | Wenn ein Parameter kritisch ist und genau so sein muss |
| anyString() | Irgendein Argument dieses Typs | Wenn der Wert egal ist, aber der Fakt des Aufrufs wichtig ist |
| startsWith("x") | Muster für einen String | Wenn die Form wichtig ist, aber nicht der vollständige Text |
| contains("x") | Muster für einen String | Wenn das Vorhandensein eines Fragments wichtig ist |
| matches(regex) | String nach regulärem Ausdruck | Wenn das Format wichtig ist, nicht der exakte String |
| argThat(predicate) | Eigene Bedingung für das Argument | Wenn ein inhaltliches Kriterium gebraucht wird, das es unter den fertigen Matchern nicht gibt |
Und jetzt — die wichtigste Regel, die Anfänger genau einmal das Leben ruiniert und dann zum „Muskelgedächtnis" wird.
Wenn man mindestens einen Matcher verwendet, müssen alle Argumente in diesem Aufruf Matcher sein.
Das heißt, folgendes ist falsch, und Mockito wirft eine InvalidUseOfMatchersException:
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
// ❌ falsch: ein Argument ist raw, das andere ist ein Matcher
verify(sender).sendPublished(42L, anyString());
So ist es richtig, weil wir das erste Argument ebenfalls in einen Matcher über eq(...) umgewandelt haben:
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
// ✅ richtig: beide Argumente sind als Matcher ausgedrückt
verify(sender).sendPublished(eq(42L), anyString());
String-Matchers: „die Form prüfen, nicht jeden Buchstaben"
Angenommen, man möchte im ContentHub beweisen, dass slug wirklich wie ein Slug aussieht und nicht wie „vergessen zu normalisieren, und ein String mit Leerzeichen ist rausgeflogen". Dann ist der Matcher matches(...) praktisch:
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.matches;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
// Prüfen: id genau 42, slug nach Vertragsformat
verify(sender).sendPublished(
eq(42L),
matches("[a-z0-9-]+")
);
Hier verlangt der Test keinen konkreten slug, aber verlangt den Sinn: nur Kleinbuchstaben, Ziffern und Bindestriche. Das ist ein gutes Beispiel für „genau nach Vertrag, aber nicht nach Kosmetik".
argThat(...): Wenn der Sinn wichtig ist, nicht der Typ
Manchmal ist es praktischer, nicht „beginnt mit…" oder „enthält…" zu sagen, sondern eine Regel als kleine Funktion zu beschreiben. Zum Beispiel: bei der Moderation wollen wir sicherstellen, dass der Text nach trim() wirklich nicht leer ist und dass die articleId richtig übergeben wurde.
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class ModerationRequestMatcherTest {
@Test
void moderation_request_has_meaningful_fields() {
// Arrange
ModerationClient client = mock(ModerationClient.class);
ArticleWorkflowService service = new ArticleWorkflowService(client);
// Act
service.submit(42L, " hello ");
// Assert: inhaltliche Bedingungen für das DTO prüfen, nicht "irgendetwas überhaupt"
verify(client).moderate(argThat(req ->
req.articleId() == 42L && !req.text().isBlank()
));
}
}
Das ist schon deutlich stärker als any(): Wir prüfen, dass der Wert nicht einfach „irgendwas" ist, sondern wirklich dem Sinn und Vertrag des Aufrufs entspricht.
An dieser Stelle ist Selbstdisziplin gefragt. argThat(...) ist ein mächtiges Werkzeug, aber wenn man dort komplexe Logik mit 15 Bedingungen schreibt, wird der Test zu einem kleinen Programm innerhalb eines Tests. Das ist normalerweise ein Zeichen, dass entweder die Prüfung für dieses Testniveau zu detailliert ist, oder das Argument besser als eigene Klasse oder DTO mit einfacherer Validierung aufgebaut werden sollte.
4. ArgumentCaptor: Das echte Argument holen und seinen Sinn prüfen
Matchers sind gut, wenn man die Prüfung direkt innerhalb von verify(...) formulieren kann. Aber manchmal ist das Argument komplexer: z.B. ein DTO mit mehreren Feldern, von denen einige innerhalb des Service berechnet werden, und man will es „nach der Tatsache" ansehen, statt die Prüfung in ein einzelnes Prädikat einzubauen. In solchen Momenten ist ArgumentCaptor wie eine Überwachungskamera: Man „erfasst" das echte Objekt, das in die Abhängigkeit geflogen ist, und untersucht es dann in Ruhe mit AssertJ.
Erweitern wir nun absichtlich dieses Lehrbuchfragment: Wir fügen den Autor hinzu, um zu zeigen, wo ArgumentCaptor gegenüber Matchern gewinnt.
// DTO wurde erweitert: wir senden jetzt mehr Kontext nach außen
record ModerationRequest(long articleId, String authorUsername, String text) { }
Der Service, der die Anfrage zusammenbaut, kann dann so aussehen:
class ArticleWorkflowService {
// Abhängigkeit, die in Tests über verify/then geprüft wird
private final ModerationClient moderationClient;
ArticleWorkflowService(ModerationClient moderationClient) {
this.moderationClient = moderationClient;
}
void submit(long articleId, String authorUsername, String text) {
// Kernlogik: DTO zusammenbauen und Text vor dem Senden bereinigen
moderationClient.moderate(new ModerationRequest(articleId, authorUsername, text.trim()));
}
}
Jetzt der Test mit ArgumentCaptor:
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class ModerationCaptorTest {
@Test
void submits_trimmed_text_with_correct_author() {
// Arrange
ModerationClient client = mock(ModerationClient.class);
ArticleWorkflowService service = new ArticleWorkflowService(client);
// Act
service.submit(42L, "editor1", " hello ");
// Assert: genau das Objekt erfassen, das wirklich in den externen Client geflogen ist
ArgumentCaptor<ModerationRequest> captor = ArgumentCaptor.forClass(ModerationRequest.class);
verify(client).moderate(captor.capture());
// Danach — normale Assertions, ohne "Magie" innerhalb von verify(...)
ModerationRequest sent = captor.getValue();
assertThat(sent.articleId()).isEqualTo(42L);
assertThat(sent.authorUsername()).isEqualTo("editor1");
assertThat(sent.text()).isEqualTo("hello");
}
}
Hier gibt es zwei wichtige Vorteile.
Erstens „pressen" wir keine komplexe Prüfung in argThat(...). Wir holen das Objekt heraus und prüfen es mit normalen Assertions, mit ordentlichen Fehlermeldungen. Wenn der Test fehlschlägt, sieht man genau, was nicht stimmt: authorUsername stimmt nicht überein, oder text wurde nicht getrimmt, oder articleId ist plötzlich null.
Zweitens kann man mehrere Aspekte eines Arguments prüfen, ohne die gesamte Prüfung an einer Stelle zu „klug" zu machen. Für Anfänger liest sich das viel einfacher: „aufgerufen, erfasst, geprüft."
Noch ein Bonus von ArgumentCaptor: Er kann alle Werte sammeln, wenn die Methode mehrfach aufgerufen wurde. Dafür gibt es getAllValues(). Im Backend trifft man das manchmal an, wenn ein Service mehrere Ereignisse veröffentlicht oder mehrere Benachrichtigungen sendet.
Werkzeuge kombinieren: strenge, aber nicht zerbrechliche Prüfungen
Der häufigste Anfängerfehler ist, ein Werkzeug zu wählen und damit Nägel, Schrauben und manchmal den Tischnachbarn einzuschlagen. Zum Beispiel immer anyString() verwenden („damit es nicht kaputtgeht") oder immer ArgumentCaptor verwenden („weil es beruhigender ist"). In der Praxis entstehen starke Tests meist aus einer vernünftigen Kombination: genaue Prüfungen an kritischen Stellen und inhaltliche Prüfungen dort, wo überflüssige Präzision nur stört.
Unten ist eine sehr praktische „Auswahlkarte", die hilft, nicht hängenzubleiben:
| Situation | Am besten geeignet |
|---|---|
| Wert ist bekannt und kritisch (id, Status, Fehlercode) | eq(...) oder exakter Wert ohne Matcher |
| Wert ist nach Form wichtig (slug, Header, Format) | startsWith/contains/matches |
| Wert ist nach Sinn wichtig und lässt sich bequem als Logik ausdrücken | argThat(...) |
| Argument ist komplex und man will mehrere Felder schön prüfen | ArgumentCaptor + AssertJ |
Versuchen wir, ein kleines, vollständiges Beispiel rund um eine Idee aus ContentHub zusammenzubauen: Ein Service veröffentlicht einen Artikel und sendet eine Benachrichtigung mit dem slug, den der SlugService liefert. Wir betreten kein Spring, sondern prüfen einfach die Interaktionen und den Sinn der Argumente.
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
interface SlugService {
// Reine Abhängigkeit: wandelt einen Titel in einen Slug um
String toSlug(String title);
}
class ArticlePublisher {
private final SlugService slugService;
private final PublicationNotificationSender sender;
ArticlePublisher(SlugService slugService, PublicationNotificationSender sender) {
this.slugService = slugService;
this.sender = sender;
}
void publish(long articleId, String title) {
// Wichtige Stelle: Slug kommt von einem separaten Service (Logik ausgelagert)
String slug = slugService.toSlug(title);
// Externer Effekt: Benachrichtigung nach außen senden
sender.sendPublished(articleId, slug);
}
}
class ArticlePublisherTest {
@Test
void sends_notification_with_slug_from_slug_service() {
// Arrange: Abhängigkeiten mocken
SlugService slugService = mock(SlugService.class);
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
ArticlePublisher publisher = new ArticlePublisher(slugService, sender);
// Arrange: Verhalten des Slug-Service festlegen
given(slugService.toSlug("Java Mockito")).willReturn("java-mockito");
// Act
publisher.publish(42L, "Java Mockito");
// Assert: id genau prüfen, slug erfassen und mit dem Erwarteten vergleichen
ArgumentCaptor<String> slugCaptor = ArgumentCaptor.forClass(String.class);
then(sender).should().sendPublished(eq(42L), slugCaptor.capture());
assertThat(slugCaptor.getValue()).isEqualTo("java-mockito");
}
}
Hier haben wir gleichzeitig Lesbarkeit (BDD-Stil), Präzision (genaue articleId über eq(42L)) und inhaltliche Prüfung. Über den Captor haben wir bewiesen, dass in den Sender genau der slug geflogen ist, der vom SlugService kam. So ein Test prüft die „Interna" des ArticlePublisher nicht mehr als nötig, fängt aber einen echten Fehler gut ab: z.B. wenn jemand versehentlich den title statt des slug in die Benachrichtigung schickt.
5. Typische Fehler beim Arbeiten mit Matchern und ArgumentCaptor
Fehler Nr. 1: anyString() (oder any()) fast überall.
Die beliebteste Version dieses Fehlers — anyString() fast überall einzusetzen, weil es einfacher ist und „nicht kaputtgeht." Das Ergebnis: Der Test schützt den Interaktionsvertrag nicht mehr. Der Service kann einen leeren Slug, eine falsche id, einen zufälligen Text senden — und ist trotzdem grün. Wenn man bemerkt, dass in verify(...) kaum eq(...) vorkommt, ist das ein guter Anlass innezuhalten und zu fragen: „Was beweise ich eigentlich?"
Fehler Nr. 2: Mischen von Matchern und raw-Werten in einem Aufruf.
Der Fehler sieht sehr harmlos aus: verify(sender).sendPublished(42L, anyString()). Die Logik scheint offensichtlich, aber Mockito funktioniert so nicht: Wenn ein Argument ein Matcher ist, müssen alle anderen ebenfalls Matcher sein. Das Ergebnis ist statt eines klaren, fachlichen Test-Fehlschlags eine Ausnahme von Mockito, und man verbringt Zeit damit, „was habe ich schon wieder kaputt gemacht." Das Gegenmittel ist ein Reflex: Sieht man any*, kritische Werte automatisch in eq(...) einwickeln.
Fehler Nr. 3: Das Argument mit ArgumentCaptor „bis auf die Atome sezieren".
ArgumentCaptor ist wirklich praktisch, aber man kann damit leicht anfangen, „das Argument bis auf die Atome zu sezieren." Man erfasst ein großes DTO und prüft im Test zehn Felder, einschließlich solcher, die das Verhalten in keiner Weise beeinflussen und Implementierungsdetails sind. Solche Tests werden zerbrechlich: Eine kleine Änderung am DTO oder an der Formatierung reißt die Test-Suite. Normalerweise ist es nützlich, nur die Felder zu prüfen, die wirklich die Regel oder den Integrationsvertrag ausdrücken: id, Ereignistyp, Schlüsselwerte, Format.
Fehler Nr. 4: argThat(...) als Mini-Programmiersprache benutzen.
Der vierte Fehler ist, argThat als Mini-Programmiersprache innerhalb von verify(...) einzusetzen: eine zehnzeilige Bedingung, mehrere &&, plus ein if innerhalb des Lambdas. Das senkt Lesbarkeit und Diagnosequalität drastisch. Wenn man merkt, dass das Prädikat komplex wird, ist es normalerweise besser, auf ArgumentCaptor umzusteigen und die Prüfungen auf mehrere verständliche AssertJ-Zeilen aufzuteilen. Paradoxerweise wird der Test dabei oft kürzer und verständlicher.
Fehler Nr. 5: Interaktionsprüfungen ersetzen die Verhaltensprüfung.
Manchmal prüft ein Test nur „was an den ModerationClient gesendet wurde", aber nicht, dass der Service das richtige Ergebnis zurückgegeben oder den Zustand des Domänenobjekts verändert hat. Dann wird er zum Test „wir haben irgendwo angerufen" ohne Beweis des Geschäftssinns. Mockito eignet sich hervorragend dafür, einen externen Effekt zu beweisen, aber ein guter Test beginnt trotzdem mit der Frage: „Welches Invariant oder welche Regel schütze ich?"
GO TO FULL VERSION