1. Sprawdzamy argumenty: „wywołano" ≠ „poprawnie"
W testach Mockito sam fakt wywołania zazwyczaj nie wystarczy. W realnym backendzie, i w ContentHub też, ważne jest, jakie dane wysłaliśmy na zewnątrz. Wywołać moderację można i z pustym tekstem, i z tekstem nie tego artykułu, i bez title — formalnie „wywołanie było", ale zachowanie systemu jest już inne.
Tutaj wygodniej zawęzić skupienie do wychodzącego payload. Dlatego ModerationClient poniżej celowo pokazany jest w węższej formie: przyjmuje DTO i nic nie zwraca. Interesuje nas teraz bardziej to, jakie dane wylatują na zewnątrz, niż trzymanie się jednej ustalonej sygnatury serwisu.
Wyobraźmy sobie uproszczony kawałek z naszej domeny. Jest integracja ModerationClient, która przyjmuje żądanie weryfikacji tekstu. Powiedzmy, że wysyłamy nie po prostu łańcuch, ale mały DTO. W Javie wygodnie trzymać go jako record, żeby nie pisać góry getterów.
interface ModerationClient {
// Zewnętrzna zależność: stąd „wychodzimy" poza granice naszego serwisu
void moderate(ModerationRequest request);
}
// DTO, który wylatuje do zewnętrznego systemu
record ModerationRequest(long articleId, String text) { }
A oto serwis, który formuje żądanie. Tu celowo zastawiono „typowe życie": robimy trim(), bo inaczej można wysłać tekst ze spacjami i myśleć, że jest „niepusty".
class ArticleWorkflowService {
// Zależność, którą w testach będziemy mockować
private final ModerationClient moderationClient;
ArticleWorkflowService(ModerationClient moderationClient) {
this.moderationClient = moderationClient;
}
void submit(long articleId, String text) {
// Ważny detal biznesowy: przed wysłaniem czyścimy spacje
moderationClient.moderate(new ModerationRequest(articleId, text.trim()));
}
}
Teraz przyjrzyjmy się testowi, który niby „weryfikuje wywołanie", ale sensem prawie niczego nie dowodzi:
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: przygotowujemy mock i serwis
ModerationClient client = mock(ModerationClient.class);
ArticleWorkflowService service = new ArticleWorkflowService(client);
// Act: wykonujemy działanie
service.submit(42L, " hello ");
// Assert: weryfikujemy „cokolwiek", ale zbyt słabo
verify(client).moderate(any()); // test „zgadza się na wszystko"
}
}
Test jest zielony, ale jest zielony nawet w świecie, gdzie ArticleWorkflowService wysyła articleId = 0, lub w ogóle nie robi trim(), lub wysyła tekst " " (same spacje). Czyli „wywołanie było" udowodniliśmy, a „wywołanie było sensowne" — nie.
Właśnie dlatego jednego verify(...).someMethod(...) mało: chcemy sprawdzić nie tylko fakt zadzwonienia do zależności, ale sens przekazanych danych. Do tego przydadzą się trzy narzędzia: styl BDD (BDDMockito), argument matchers i ArgumentCaptor.
2. BDDMockito: ten sam Mockito, tylko w bardziej potocznej formie
BDDMockito — to nie inna biblioteka i nie inny Mockito. To to samo narzędzie, tylko ze składnią, która czyta się jak mini-opowieść: given (dane), when (gdy), then (wtedy). Szczególnie wygodne to jest w testach na interakcje: mniej przypominają „scenariusz wywołań" i lepiej czytają się jako historia.
Żeby zobaczyć różnicę, wygodnie porównać „klasyczny" i BDD-wariant w tabeli:
| Co robimy | Klasyczny Mockito | BDDMockito |
|---|---|---|
| Konfigurujemy odpowiedź zależności | when(x.m()).thenReturn(v) | given(x.m()).willReturn(v) |
| Konfigurujemy wyjątek | when(x.m()).thenThrow(e) | given(x.m()).willThrow(e) |
| Weryfikujemy wywołanie | verify(x).m() | then(x).should().m() |
| Weryfikujemy liczbę wywołań | verify(x, times(2)).m() | then(x).should(times(2)).m() |
Z punktu widzenia JVM i Mockito pod spodem nic się nie zmienia. Zmienia się tylko „język", w którym opowiadasz test.
Oto jak może wyglądać BDD-weryfikacja tego, że tekst przed wysłaniem do moderacji jest rzeczywiście trymowany:
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: weryfikujemy, że wyleciało właśnie "hello", a nie " hello "
then(client).should().moderate(new ModerationRequest(42L, "hello"));
}
}
Zwróć uwagę na małą, ale przyjemną rzecz: then(client).should() bardzo dobrze wizualnie oddziela weryfikację interakcji od reszty testu. Dla nowicjuszy to często zmniejsza zamieszanie: gdzie konfigurujemy zachowanie, gdzie wywołujemy metodę, a gdzie sprawdzamy wynik.
Jeszcze jedna praktyczna zaleta stylu BDD: dobrze współpracuje ze zwykłą strukturą Arrange – Act – Assert, ale sprawia, że bloki są trochę bardziej jawne na poziomie języka. W Arrange zazwyczaj używamy given(...), a w Assert — then(...).should(...). Test zaczyna wyglądać jak historia, a nie jak rytuał z zaklęciami.
3. Argument matchers: precyzja bez mikrozarządzania
Tutaj zazwyczaj włącza się „tryb uczciwego perfekcjonisty". Z jednej strony chce się sprawdzać argumenty dokładnie, żeby test naprawdę łapał defekty. Z drugiej — jeśli sprawdzać wszystko przez pełną równość, testy szybko stają się kruche: każdy kosmetyczny refaktor łańcucha, formatowania tekstu lub dodatkowego pola zaczyna łamać połowę zestawu testów. Argument matchers pomagają utrzymać równowagę: powiedzieć „ta wartość jest krytyczna, a tutaj ważna jest tylko cecha znaczeniowa".
Zacznijmy od małej sytuacji z ContentHub. Niech przy publikacji artykułu wysyłamy powiadomienie do PublicationNotificationSender. Z punktu widzenia testu czasem ważne jest, że powiadomienie poszło właśnie dla articleId = 42, ale dokładny slug może się zmieniać z powodu małych reguł normalizacji (myślniki, kropki, dodatkowe sufiksy). Wtedy przydatne jest sprawdzenie: id — dokładny, a slug — przynajmniej według wzorca.
interface PublicationNotificationSender {
// Integracja/port: stąd wylatuje powiadomienie o publikacji
void sendPublished(long articleId, String slug);
}
Najbardziej przydatne matchery: mini-ściągawka
Żeby nie zamieniać lektury w instrukcję Mockito, zbieramy najbardziej przydatne matchery w małej tabeli — dokładnie te, które realnie spotykają się w zwykłych testach backendowych:
| Matcher | Co utrwala | Kiedy przydatny |
|---|---|---|
| eq(value) | dokładna wartość | gdy parametr jest krytyczny i musi być dokładnie taki |
| anyString() | dowolny argument tego typu | gdy wartość nieważna, ale fakt wywołania ważny |
| startsWith("x") | wzorzec dla łańcucha | gdy ważna forma, ale nie pełny tekst |
| contains("x") | wzorzec dla łańcucha | gdy ważna obecność fragmentu |
| matches(regex) | łańcuch według wyrażenia regularnego | gdy ważny format, a nie dokładny łańcuch |
| argThat(predicate) | własny warunek na argument | gdy potrzebne kryterium znaczeniowe, którego nie ma wśród gotowych matcherów |
I teraz — najważniejsza zasada, która nowicjuszom psuje życie dokładnie raz, a potem zamienia się w „pamięć mięśniową".
Jeśli używasz choćby jednego matchera, to wszystkie argumenty w tym wywołaniu muszą być matcherami.
Czyli tak robić nie wolno, i Mockito rzuci InvalidUseOfMatchersException:
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
// ❌ niepoprawnie: jeden argument raw, drugi matcher
verify(sender).sendPublished(42L, anyString());
A tak — poprawnie, bo pierwszy argument też zamieniliśmy w matcher przez eq(...):
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);
// ✅ poprawnie: oba argumenty wyrażone jako matchery
verify(sender).sendPublished(eq(42L), anyString());
Matchery łańcuchowe: „sprawdzamy formę, a nie każdą literę"
Powiedzmy, że w ContentHub chcesz udowodnić, że slug rzeczywiście wygląda jak slug, a nie jak „zapomniano normalizować, i wyleciał łańcuch ze spacjami". Wtedy wygodny jest matcher matches(...):
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);
// Weryfikujemy: id dokładnie 42, a slug — według kontraktu formatu
verify(sender).sendPublished(
eq(42L),
matches("[a-z0-9-]+")
);
Tutaj test nie wymaga konkretnego slug-a, ale wymaga sensu: tylko małe litery, cyfry i myślniki. To dobry przykład „rygorystycznie według kontraktu, ale nie kosmetyki".
argThat(...): gdy ważny jest sens, a nie typ
Czasem wygodniej powiedzieć nie „zaczyna się od…" i nie „zawiera…", ale opisać regułę jako małą funkcję. Na przykład dla moderacji chcemy się upewnić, że tekst jest naprawdę niepusty po trim() i że articleId przekazano właściwy.
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: weryfikujemy znaczeniowe warunki na DTO, a nie „cokolwiek"
verify(client).moderate(argThat(req ->
req.articleId() == 42L && !req.text().isBlank()
));
}
}
To już dużo mocniejsze niż any(): sprawdzamy, że wartość nie jest po prostu „jakaś", ale rzeczywiście odpowiada sensowi i kontraktowi wywołania.
W tym miejscu ważne jest powściąganie siebie. argThat(...) — to potężne narzędzie, ale jeśli zaczniesz pisać tam złożoną logikę na 15 warunków, test zamienia się w mały program wewnątrz testu. Zazwyczaj to znak, że albo weryfikacja jest zbyt szczegółowa dla poziomu testu, albo argument lepiej zrobić osobną klasą lub DTO z prostszą walidacją.
4. ArgumentCaptor: wyciągamy prawdziwy argument i sprawdzamy jego sens
Matchery są dobre, gdy możesz sformułować weryfikację bezpośrednio wewnątrz verify(...). Ale czasem argument jest bardziej złożony: na przykład to DTO z kilkoma polami, część z których jest obliczana wewnątrz serwisu, i chcesz na niego spojrzeć „po fakcie", a nie wbudowywać weryfikację w jeden predykat. W takich momentach ArgumentCaptor — jak kamera monitorująca: „przechwytujemy" prawdziwy obiekt, który wyleciał do zależności, a potem spokojnie analizujemy go przez AssertJ.
Teraz specjalnie rozszerzamy ten sam szkolny fragment: dodajemy autora, żeby pokazać, gdzie ArgumentCaptor wygrywa nad matcherami.
// DTO rozszerzone: teraz wysyłamy więcej kontekstu na zewnątrz
record ModerationRequest(long articleId, String authorUsername, String text) { }
Wtedy serwis, który formuje żądanie, może wyglądać tak:
class ArticleWorkflowService {
// Zależność, którą w testach będziemy weryfikować przez verify/then
private final ModerationClient moderationClient;
ArticleWorkflowService(ModerationClient moderationClient) {
this.moderationClient = moderationClient;
}
void submit(long articleId, String authorUsername, String text) {
// Kluczowa logika: składamy DTO i czyścimy tekst przed wysłaniem
moderationClient.moderate(new ModerationRequest(articleId, authorUsername, text.trim()));
}
}
Teraz test z 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: przechwytujemy właśnie ten obiekt, który realnie wyleciał do zewnętrznego klienta
ArgumentCaptor<ModerationRequest> captor = ArgumentCaptor.forClass(ModerationRequest.class);
verify(client).moderate(captor.capture());
// Dalej — zwykłe asserty, bez „magii" wewnątrz verify(...)
ModerationRequest sent = captor.getValue();
assertThat(sent.articleId()).isEqualTo(42L);
assertThat(sent.authorUsername()).isEqualTo("editor1");
assertThat(sent.text()).isEqualTo("hello");
}
}
Tutaj są dwie ważne zalety.
Po pierwsze, nie „wpychamy" złożonej weryfikacji do argThat(...). Wyciągamy obiekt i sprawdzamy go zwykłymi assertions, z normalnymi komunikatami o błędach. Gdy test padnie, zobaczysz, co dokładnie nie pasuje: authorUsername się nie zgadza, lub text nie był trim()-owany, lub articleId nagle zerowy.
Po drugie, możemy sprawdzać kilka aspektów jednego argumentu, nie robiąc całej weryfikacji zbyt „sprytną" w jednym miejscu. Dla nowicjusza czyta się to o wiele prościej: „wywołano, przechwycono, sprawdzono".
Jeszcze jeden bonus ArgumentCaptor: umie zbierać wszystkie wartości, jeśli metoda była wywoływana kilka razy. Do tego jest getAllValues(). Czasem w backendzie się to pojawia, gdy serwis publikuje kilka zdarzeń lub wysyła kilka powiadomień.
Łączymy narzędzia: rygorystyczne, ale nie kruche weryfikacje
Najczęstszy błąd nowicjusza — wybrać jedno narzędzie i zacząć nim wbijać gwoździe, śruby i czasem sąsiada w ławce. Na przykład zawsze używać anyString() („żeby się nie łamało") lub zawsze używać ArgumentCaptor („bo tak spokojniej"). W rzeczywistości silne testy zazwyczaj wychodzą z rozsądnego połączenia: dokładne weryfikacje w krytycznych miejscach i znaczeniowe weryfikacje tam, gdzie nadmierna precyzja tylko przeszkadza.
Poniżej — bardzo praktyczna „mapa wyboru", która pomaga się nie zawiesić:
| Sytuacja | Najlepiej nadaje się |
|---|---|
| Wartość znana i krytyczna (id, status, kod błędu) | eq(...) lub dokładna wartość bez matcherów |
| Wartość ważna według formy (slug, header, format) | startsWith/contains/matches |
| Wartość ważna znaczeniowo i wygodnie wyrazić logiką | argThat(...) |
| Argument złożony i chcesz sprawdzić kilka pól ładnie | ArgumentCaptor + AssertJ |
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 {
// Czysta zależność: zamienia tytuł w slug
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) {
// Ważne miejsce: slug pobieramy z osobnego serwisu (logika wydzielona)
String slug = slugService.toSlug(title);
// Zewnętrzny efekt: wysyłamy powiadomienie na zewnątrz
sender.sendPublished(articleId, slug);
}
}
class ArticlePublisherTest {
@Test
void sends_notification_with_slug_from_slug_service() {
// Arrange: mockujemy zależności
SlugService slugService = mock(SlugService.class);
PublicationNotificationSender sender = mock(PublicationNotificationSender.class);
ArticlePublisher publisher = new ArticlePublisher(slugService, sender);
// Arrange: zadajemy zachowanie serwisu slug
given(slugService.toSlug("Java Mockito")).willReturn("java-mockito");
// Act
publisher.publish(42L, "Java Mockito");
// Assert: weryfikujemy id rygorystycznie, slug — przechwytujemy i porównujemy z oczekiwanym
ArgumentCaptor<String> slugCaptor = ArgumentCaptor.forClass(String.class);
then(sender).should().sendPublished(eq(42L), slugCaptor.capture());
assertThat(slugCaptor.getValue()).isEqualTo("java-mockito");
}
}
Tutaj jednocześnie dostajemy czytelność (styl BDD), rygorystyczność (dokładny articleId przez eq(42L)) i znaczeniową weryfikację. Przez captor udowodniliśmy, że do sendera wyleciał właśnie slug otrzymany od SlugService. Taki test nie sprawdza „wnętrzności" ArticlePublisher bardziej niż potrzeba, ale świetnie łapie realny błąd: na przykład jeśli ktoś przypadkowo wyśle w powiadomieniu title zamiast slug.
5. Typowe błędy przy pracy z matcherami i ArgumentCaptor
Błąd nr 1: anyString() (lub any()) prawie wszędzie.
Najczęstsza wersja tego błędu — stawianie anyString() prawie wszędzie, bo tak prościej i „się nie łamie". W efekcie test już nie chroni kontraktu interakcji: serwis może wysyłać pusty slug, błędny id, przypadkowy tekst — i nadal będzie zielony. Jeśli łapiesz się na tym, że w verify(...) prawie nie ma eq(...), to dobry moment zatrzymać się i zapytać: „a co dokładnie udowadniam?".
Błąd nr 2: mieszanie matcherów i raw-wartości w jednym wywołaniu.
Błąd wygląda bardzo niewinnie: verify(sender).sendPublished(42L, anyString()). Wydaje się, że logika jest oczywista, ale Mockito tak nie działa: jeśli jeden argument to matcher, pozostałe też muszą być matcherami. W efekcie zamiast jasnego padnięcia testu z przyczyn biznesowych dostajesz wyjątek od Mockito i tracisz czas na „co znowu zepsułem". Leczy to jeden odruch: zobaczyłeś any* — automatycznie owijaj krytyczne wartości w eq(...).
Błąd nr 3: „preparowanie argumentu do atomów" przez ArgumentCaptor.
ArgumentCaptor jest rzeczywiście wygodny, ale łatwo zacząć „preparować argument do atomów". Przechwytуjesz duże DTO i w teście weryfikujesz dziesięć pól, łącznie z tymi, które w żaden sposób nie wpływają na zachowanie i są detalem implementacji. Takie testy stają się kruche: mała zmiana DTO lub formatowania rozbija zestaw testów. Zazwyczaj przydatne jest sprawdzanie tylko tych pól, które rzeczywiście wyrażają regułę lub kontrakt integracji: id, typ zdarzenia, kluczowe wartości, format.
Błąd nr 4: używanie argThat(...) jako mini-języka programowania.
Czwarty błąd — używanie argThat jako mini-języka programowania wewnątrz verify(...): warunek na dziesięć linii, kilka &&, plus if wewnątrz lambdy. Mocno obniża to czytelność i jakość diagnostyki. Jeśli czujesz, że predykat staje się złożony, zazwyczaj lepiej przejść na ArgumentCaptor i rozłożyć weryfikacje na kilka zrozumiałych linii AssertJ. Paradoksalnie, ale test często staje się i krótszy, i bardziej zrozumiały.
Błąd nr 5: weryfikacja interakcji zastępuje weryfikację zachowania.
Czasem test sprawdza tylko „co wysłano do ModerationClient", ale nie sprawdza, że serwis zwrócił właściwy wynik lub zmienił stan obiektu domenowego. Wtedy zamienia się w test „gdzieś zadzwoniliśmy" bez dowodu biznesowego sensu. Mockito świetnie nadaje się do udowodnienia zewnętrznego efektu, ale dobry test nadal zaczyna się od pytania: „jaki niezmiennik lub regułę chronię?".
GO TO FULL VERSION