CodeGym /Kursy /Spring Test /Argumenty w Mockito: matchers i captor

Argumenty w Mockito: matchers i captor

Spring Test
Poziom 4, Lekcja 2
Dostępny

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ę?".

1
Zadanie
Spring Test, poziom 4, lekcja 2
Niedostępne
BDDMockito i matchery do wysyłania powiadomień
BDDMockito i matchery do wysyłania powiadomień
1
Zadanie
Spring Test, poziom 4, lekcja 2
Niedostępne
Przechwytywanie DTO za pomoca ArgumentCaptor
Przechwytywanie DTO za pomoca ArgumentCaptor
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION