Jak testować wysyłkę maila w Spring-Boot

0

Cześć wszystkim, chciałbym was prosić o poradę. Piszę sobie do szuflady aplikację webową. Stack to Java i Spring-Boot i do testowania używam Spocka.
Mam moduł dotyczący użytkownika i wszystko jest w nim package-scope oprócz fasady, która jest punktem wejścia modułu. Proces wygląda następująco:

  • Rejestracja użytkownika
  • Stworzenie tokena weryfikacyjnego rejestrację
  • Następnie druga fasada z drugiego modułu NotificationFacade jest wstrzyknięta do UserFacade i tam przekazuje jej dane, kto się zarejestrował, czyli email i username oraz token do wysyłki
  • NotificationFacade ma metodę do wysyłki linków aktywacyjnych po rejestracji, buduje link aktywacyjny i wysyła wiadomość z pomocą JavaMailSender do użytkownika i na końcu zmienia status Usera w postaci enuma na aktywowane
  • Cały proces odbywa się w metodzie do rejestracji użytkownika

Stosuje podejście Kuby Nabrdalika z Hexagonal Architecture. Wszystko było pięknie, napisałem sobie testy jednostkowe z bazą in memory, ale nagle doszła mi integracja z SMTP do wysyłki emaili i mam problem, bo wszystko jest zamknięte w metodzie do rejestracji użytkownika i teraz mam wrażenie, że to wszystko powinienem przetestować integracyjnie na jakimś fake SMTP server czy coś takiego. Gryzie mi się to z testami jednostkowymi, bo w tych testach jednostkowych korzystam z bazy na HashMapie i nie chcę tam żadnego zewnętrznego kontekstu podnosić. Chciałbym sprawdzić zanim użytkownik potwierdzi rejestrację, czy token jest ważny, czy użytkownik, który potwierdza rejestrację nie próbuję potwierdzić rejestracji, która jest już potwierdzona itp. Zwracam tam Eithery z błędami.
Nie wiem czy doszedłem do poprawnych wniosków, ale wydaję mi się, że całość tego procesu powinienem przetestować integracyjne z jakimś TestContainers i fake SMTP.
Zanim dopisałem sobie feature do wysyłki email, miałem metodę na UserFacade do rejestracji użytkownika, tworzenia tokena i potwierdzenia rejestracji. Bardzo łatwo to przyszło testować in memory. Ale teraz musiałem to wszystko zamknąć w jednym procesie, którym jest rejestracja i podpiąć wysyłkę emaila. Została mi metoda do potwierdzania rejestracji, która zatwierdza rejestrację jak użytkownik kliknie w link aktywacyjny i przekazywany jest tam token jako argument. Teraz jak to przetestować jednostkowo jak podczas rejestracji jest tworzony ten token i zapisywany w bazie, nie mogę chyba tego tokena zwracać po rejestracji jako jakieś DTO, prawda? Bo wtedy udostępniam ten token tylko na potrzeby testów. Mogłbym mieć publiczną metodę na fasadzie do tworzenia tokena i tworzyć ten token w testach, ale czy to jest dobra praktyka Oczywiście nie testuje implementacji, tylko chcę przetestować zachowanie tego procesu.

Pozdrawiam!

0

Nie czytam ściany tekstu, jestem ostatnio zbyt leniwy.

EMail można wysyłać przez tak zwany pickup directory.
Jak to zrobić w Java (to jest zapisać email w pliku): https://stackoverflow.com/questions/8167583/create-an-email-object-in-java-and-save-it-to-file

W ten sposób możesz sobie przetestować cały flow.

Generalnie brzmi jak przekombinowane i overengineerowane coś. "podejście Kuby Nabrdalika z Hexagonal Architecture" - nie jest stosowane ani w Google ani w Uber ani w Netflix. Lokalsi w korpo stosują gdzie mają 1% ruchu Ubera.

1

Wyślij go za pomocą testowego konta na gmailu (czy jakiego tam innego servera chcesz uzyc produkcyjnie" i sprawdz czy dotarł. Z bazami in memory i chorobami typu "mockoza" trzeba ostrożnie.

PS: Credentiali nie musisz trzymac w kodzie.

1

Jak to zrobić technicznie napisali koledzy wyżej. Ja tylko przyczepię się szczegółu w jaki sposób zamodelowałeś ten proces w kodzie. Otóż z tego co opisałeś, to powinieneś mieć jeden moduł UserRegistration, który ma w sobie logikę rejestracji użytkownika i wysyłania powiadomienia za pomocą wstrzykiwalnego serwisu SmtpService który odpowiada za komunikację z serwerem SMTP i nie ma żadnej logiki biznesowej, tak jak przykładowo metoda Save jakiegoś repozytorium, która dostaje obiekt i jedynym jej zadaniem jest utrwalić ten obiekt w storage. Nie powinieneś mieć dwóch osobnych modułów w tym kształcie, bo twój moduł powiadomień jest biznesowo związany z rejestracją użytkownika - tworzy link weryfikacyjny, aktywuje użytkownika, itd. Jak coś zmienisz w jednym module to musisz automatycznie poprawiać w drugim.

Sam fakt tego, że wstrzykujesz jeden moduł do drugiego już wskazuje na zły podział odpowiedzialności. Moduły powinny być od siebie jak najbardziej niezależne. Co ci z modularności jak przy każdej zmianie będziesz musiał zmieniać trzy inne moduły? Co daje taka separacja? Nie mówiąc o tym, że wraz z rozwojem systemu będziesz dodawał kolejne typy powiadomień i w ten oto sposób moduł notyfikacji stanie się sercem twojej aplikacji bo wszystkie inne moduły będą miały do niego zależność, a to bardzo złe jest.

To co zrobiłeś to przeciwieństwo package by feature, bo feature w nazwie to wymaganie biznesowe/use case/przypadek użycia i tak powinieneś dzielić kod. Czyli UserRegistration, UserProfileUpdate, AccountSupsension, itd.

1

https://greenmail-mail-test.github.io/greenmail/ opcjonalnie uruchamiany z pomocą https://testcontainers.com/

Mamy 2024 rok i bazy danych są naprawdę lekkie i spakowane w dockera - nie trzeba ich re implementować jako HashMapy. Testuj funkcje biznesowe, a nie klasy z prawdziwą lekką infrastruktura. Jednostka to funkcja. Proste

0
markone_dev napisał(a):

Jak to zrobić technicznie napisali koledzy wyżej. Ja tylko przyczepię się szczegółu w jaki sposób zamodelowałeś ten proces w kodzie. Otóż z tego co opisałeś, to powinieneś mieć jeden moduł UserRegistration, który ma w sobie logikę rejestracji użytkownika i wysyłania powiadomienia za pomocą wstrzykiwalnego serwisu SmtpService który odpowiada za komunikację z serwerem SMTP i nie ma żadnej logiki biznesowej, tak jak przykładowo metoda Save jakiegoś repozytorium, która dostaje obiekt i jedynym jej zadaniem jest utrwalić ten obiekt w storage. Nie powinieneś mieć dwóch osobnych modułów w tym kształcie, bo twój moduł powiadomień jest biznesowo związany z rejestracją użytkownika - tworzy link weryfikacyjny, aktywuje użytkownika, itd. Jak coś zmienisz w jednym module to musisz automatycznie poprawiać w drugim.

Sam fakt tego, że wstrzykujesz jeden moduł do drugiego już wskazuje na zły podział odpowiedzialności. Moduły powinny być od siebie jak najbardziej niezależne. Co ci z modularności jak przy każdej zmianie będziesz musiał zmieniać trzy inne moduły? Co daje taka separacja? Nie mówiąc o tym, że wraz z rozwojem systemu będziesz dodawał kolejne typy powiadomień i w ten oto sposób moduł notyfikacji stanie się sercem twojej aplikacji bo wszystkie inne moduły będą miały do niego zależność, a to bardzo złe jest.

To co zrobiłeś to przeciwieństwo package by feature, bo feature w nazwie to wymaganie biznesowe/use case/przypadek użycia i tak powinieneś dzielić kod. Czyli UserRegistration, UserProfileUpdate, AccountSupsension, itd.

markone_dev napisał(a):

Jak to zrobić technicznie napisali koledzy wyżej. Ja tylko przyczepię się szczegółu w jaki sposób zamodelowałeś ten proces w kodzie. Otóż z tego co opisałeś, to powinieneś mieć jeden moduł UserRegistration, który ma w sobie logikę rejestracji użytkownika i wysyłania powiadomienia za pomocą wstrzykiwalnego serwisu SmtpService który odpowiada za komunikację z serwerem SMTP i nie ma żadnej logiki biznesowej, tak jak przykładowo metoda Save jakiegoś repozytorium, która dostaje obiekt i jedynym jej zadaniem jest utrwalić ten obiekt w storage. Nie powinieneś mieć dwóch osobnych modułów w tym kształcie, bo twój moduł powiadomień jest biznesowo związany z rejestracją użytkownika - tworzy link weryfikacyjny, aktywuje użytkownika, itd. Jak coś zmienisz w jednym module to musisz automatycznie poprawiać w drugim.

Sam fakt tego, że wstrzykujesz jeden moduł do drugiego już wskazuje na zły podział odpowiedzialności. Moduły powinny być od siebie jak najbardziej niezależne. Co ci z modularności jak przy każdej zmianie będziesz musiał zmieniać trzy inne moduły? Co daje taka separacja? Nie mówiąc o tym, że wraz z rozwojem systemu będziesz dodawał kolejne typy powiadomień i w ten oto sposób moduł notyfikacji stanie się sercem twojej aplikacji bo wszystkie inne moduły będą miały do niego zależność, a to bardzo złe jest.

To co zrobiłeś to przeciwieństwo package by feature, bo feature w nazwie to wymaganie biznesowe/use case/przypadek użycia i tak powinieneś dzielić kod. Czyli UserRegistration, UserProfileUpdate, AccountSupsension, itd.

markone_dev napisał(a):

Jak to zrobić technicznie napisali koledzy wyżej. Ja tylko przyczepię się szczegółu w jaki sposób zamodelowałeś ten proces w kodzie. Otóż z tego co opisałeś, to powinieneś mieć jeden moduł UserRegistration, który ma w sobie logikę rejestracji użytkownika i wysyłania powiadomienia za pomocą wstrzykiwalnego serwisu SmtpService który odpowiada za komunikację z serwerem SMTP i nie ma żadnej logiki biznesowej, tak jak przykładowo metoda Save jakiegoś repozytorium, która dostaje obiekt i jedynym jej zadaniem jest utrwalić ten obiekt w storage. Nie powinieneś mieć dwóch osobnych modułów w tym kształcie, bo twój moduł powiadomień jest biznesowo związany z rejestracją użytkownika - tworzy link weryfikacyjny, aktywuje użytkownika, itd. Jak coś zmienisz w jednym module to musisz automatycznie poprawiać w drugim.

Sam fakt tego, że wstrzykujesz jeden moduł do drugiego już wskazuje na zły podział odpowiedzialności. Moduły powinny być od siebie jak najbardziej niezależne. Co ci z modularności jak przy każdej zmianie będziesz musiał zmieniać trzy inne moduły? Co daje taka separacja? Nie mówiąc o tym, że wraz z rozwojem systemu będziesz dodawał kolejne typy powiadomień i w ten oto sposób moduł notyfikacji stanie się sercem twojej aplikacji bo wszystkie inne moduły będą miały do niego zależność, a to bardzo złe jest.

To co zrobiłeś to przeciwieństwo package by feature, bo feature w nazwie to wymaganie biznesowe/use case/przypadek użycia i tak powinieneś dzielić kod. Czyli UserRegistration, UserProfileUpdate, AccountSupsension, itd.

@markone_dev:

Tak wygląda struktura mojego modułu, publiczna jest tylko fasada. Wszystko co związane z operacjami na koncie użytkownika jest w tym module, cała rejestracja, po zmianę hasła, nadanie użytkownikowi nowego uprawnienia itp. Za wysyłkę linku aktywacyjnego mam moduł Notification i tam klasę do zbudowania linku aktywacyjnego dla rejestracji. I mam tak jak mówisz, że do UserRegistration powinien być wstrzyknięty jakiś SMTP service. U mnie to jest NotificationFacade wstrzyknięte do UserRegistration. VerificationRegistration jest wstrzyknięte do UserRegistration, bo w VerificationRegistration mam cały proces tworzenia token i jego potwierdzenia. Nie pomyślałem właśnie wcale, że co będzie jak dojdą mi kolejne powiadomienia z jakiś innych modułów i będę musiał to wstrzykiwać wszędzie i zrobi się spaghetti i nic mi po tej modularności.
Może zrobię package Notification w tym swoim module User i tam zajmę się cała operacją tworzenia tego tokena i wysyłki, a w pakiecie infrastructure ogarnę cała integrację z tym API SMTP i konfigurację, co o tym myślisz?
Będzie to wtedy zamknięte w obrębie jednego modułu.
Doszedłem jeszcze do jednego wniosku, nie wiem czy poprawnego, że trochę te moje testy jednostkowe w pamięci zaczynają się robić kulą u nogi w moim projekcie. Bo mam teraz proces rejestracji użytkownika i on składa się z kilku mniejszych procesów, jak zapis użytkownika do bazki, stworzenie tokena i wysłanie powiadomienia z linkiem weryfikacyjnym, ale teraz myślę, że to nie jest jakaś skomplikowana logika biznesowa na potrzeby testów jednostkowych. I tak jak pierwsze dwa procesy mogłem przetestować bez problemu tak już do wysyłki emali potrzebuje jakiegoś TestContainers czy czegoś innego co mogłoby mi zasymulować wysyłkę emaila dla potrzeb testu.
Myślę, że tutaj sprawdzą jest bardziej testy integracyjne albo E2E.
Zrzut ekranu z 2024-04-06 10-18-49.png

Dzięki wszystkim za cenne wskazówki!

0

@markone_dev Tak wygląda moja fasada.

@RequiredArgsConstructor
public class UserFacade {
    private final UserMapper userMapper;
    private final UserManager userManager;
    private final UserUpdater userUpdater;
    private final UserPromoter userPromoter;
    private final UserRegistration userRegistration;
    private final VerificationRegistration verificationRegistration;

    public Either<UserError, RegistrationResponse> registerAsUser(final RegistrationRequest userRequest) {
        return userRegistration.registerUser(userRequest);
    }

    public Either<UserError, UserPublicDetailsDto> confirmRegistration(final String token) {
        return verificationRegistration.confirmVerificationToken(token);
    }

    public Either<UserError, UserLoginDto> findByUsername(final String username) {
        return userManager.getUserByUsername(username)
                .map(userMapper::toLoginDto)
                .toEither(USER_NOT_FOUND);
    }

    public Either<UserError, UserPublicDetailsDto> findUserById(final Long userId) {
        return userManager.findUserById(userId)
                .map(userMapper::toUserDetailsDto)
                .toEither(USER_NOT_FOUND);
    }

    public List<UserPublicDetailsDto> fetchAllUsers(final int page) {
        return userManager.fetchAllUsersWithPageable(page)
                .stream()
                .map(userMapper::toUserDetailsDto)
                .collect(Collectors.toList());
    }

    public Either<UserError, UserPublicDetailsDto> promoteToAdmin(final Long userId) {
        return userPromoter.promoteToAdmin(userId);
    }

    public Either<UserError, UserPublicDetailsDto> changeUsername(final Long userId, final String newUsername) {
        return userUpdater.changeUsername(userId, newUsername);
    }

    public void removeUserById(final Long userId) {
        userManager.removeUserById(userId);
    }
}
0

Na pytanie: "Jak testować wysyłkę maila w Spring-Boot?" odpowiedź brzmi: Dokładnie tak samo jak każdą jedną rzeczy która jest na krawędzi Twojego systemu.

  1. Nałóż abstrakcję na wysyłkę maili - tak żeby idealnie był jeden interfejs, np taki Mailer.send("[email protected]", "subject", "message").
  2. Wszystkie testy w swojej aplikacji powinny skorzystać z fejkowej implementacji, np new FakeMailer(), który daje metody na których możesz pisać asercję, np Mailer.getSentMails()
  3. To powinno pokryć 99% Twoich przypadków.
  4. Jeśli chcesz przetestować samą wysyłkę maili, to:
    1. jedyny sposób żeby to zrobić to chyba wstrzyknąć klienta HTTP i zrobić asercje czy odpowiedni output leci
    2. albo postawić swój serwer smpt, odebrać maila i sprawdzić w ten sposób, tylko to jest w sumie wolniejsze i łatwiej to zepsuć.

1 użytkowników online, w tym zalogowanych: 0, gości: 1