Mam pytanie. Korzystając z adnotacji @Valid
w kontrolerze, również tam odbywa się walidacja moich pól. Żeby zrobić te testy tak jak napisałeś, wychodzi na to, że muszę przenieś walidację do serwisu i zrezygnować z używania adnotacji @Valid
. Dobrze rozumiem?
Bardzo dobre pytanie, i tutaj pojawia się całe serce problemu. Od decyzji którą teraz podejmiesz, będzie zależała jakość Twojej aplikacji.
Pytanie brzmi: "Co sprawdzasz tą adnotacją @Valid
"? Czy sprawdzasz rzeczy które są apliaction-specific (zależą od natury aplikacji, czy jest webowa, cli, desktopwa, mobilna etc.), czy zależą od biznesowych kryteriów?
Jeśli @Valid
sprawdza takie rzeczy jak:
- To czy int ma określoną wielkośc
- Czy argument jest w body, w query, czy w headerach
- Sprawdza format (np json, xml, yaml, etc.)
- Sprawdza mime type z headerów
- To czy niektóre atrybuty spełniają jakieś kryteria zalezne od formy, np czy int jest ujemny
- Sprawdzenie wypełnienia captchy
- Sprawdzenie CSRF
- Czy otrzymany JSON/XML nie jest przypadkiem malformed
- Sprawdzenie takich rzeczy jak nagłówki
Host
, Remote
, Origin
, CORS etc.
- Czy hasło użytkownika jest stringiem, a nie intem
to test pod to powinien znajdować się w teście kontrolera, bo te rzeczy są związane z interfejsem komunikacyjnym, w tym wypadku HTTP. To są rzeczy na które użytkownik (nietechniczny) nie zwróci uwagi. Dla niego mogłyby być, mogłyby nie być, nie ma to znaczenia. (ma znaczenie z technicznych powodów - musimy je dodać, ale nie dla użytkownika).
Jeśli natomiast Twój @Valid
miałby sprawdzać takie rzeczy jak:
- To czy użytkownik jest zalogowany
- czy jego nick się składa z odpowiednich znaków
- czy jakieś pole (jak login) jest unikalne w aplikacji
- podczas pokazania jakiegoś zasobu, to czy ten zasób istnieje
- czy dane które podał są poprawne z uwagi na ich treść (nie formę), jak np poprawny pesel
- czy użytkownik ma np 18 lat żeby zobaczyć jakąś treść
- czy ma na tyle punktów/reputacji żeby wykonać akcję
- czy hasło użytkownika się zgadza i jest poprawne
to testy pod nie powinny się znaleźć w teście logiki - bo te rzeczy są poprawne, i nie zależą od interfejsu tylko od natury Twojej aplikacji. To są rzeczy na które klient/użytkownik zwróci uwagę.
Jesli tak się dzieje że Twoja adnotacja @Valid
sprawdza obie te rzeczy: tzn. trochę tego trochę tego, np sprawdza poprawność typów ORAZ tego czy zasób istnieje w bazie; to najlepiej byłoby to rozbić na dwa byty - to co jest wspólnego z HTTP zostawić w @Valid
, a to co jest niezwiązane z interfejsem przenieść do serwisów. Nie jest to proste do zrobienia - wymaga od Ciebie mentalnego wysiłku, żeby się zastanowić hmm... to pole tutaj, to jest część interfejsu, czy część biznesów?. I ten właśnie wysiłek mentalny który włożysz w rozkmienienie tego, to jest to co zwiększy jakość Twojej aplikacji.
Weź pod uwagę, że te rzeczy nie są nigdy 1:1. To od Ciebie zależy jak je podzielisz. Np napisałem że Sprawdza format (np json, xml, yaml, etc.)
to jest elementu interfejsu HTTP, bo tak jest w większości przypadków. Ale jeśli pracowałbym np na edytorem JSON (coś jak notepad++), to wtedy poprawność składni jaknajbardziej byłaby logiką biznesową, i jej sprawdzenie nie powinno być w kontrolerze. Napisałem też np że To czy int ma określoną wielkośc
to jest element interfejsu HTTP; bo tak jest w większości aplikacji. Ale jeśli pisalibyśmy np apkę pomagającą w konwersji typów (np jakiś konwrter z C na JS), to wtedy wielkość liczb byłaby bardzo ważna! Trzeba by koniecznie umieścić to w logice biznesowej. Działa to też w drugą stronę, napisałem że format nicku to logika biznesowa czy jego nick się składa z odpowiednich znaków
; ale oczywiście możemy mieć interfejs który pozwala tylko na konkretne znaki, i wtedy ich format to powinien być artefakt interfejsu - i powinien być w kontrolerze. Więc widzisz, cała ta rozdziałka to jest Twój obowiązek, na zdecydowanie które zasady są "dlatego że muszą być" (logika biznesowa), a które są "dlatego że korzystamy z HTTP" (logika kontrolera). Do Ciebie należy zdecydowanie o tym, po co dodajesz jakiś kod. Jeśli dodajesz kod, dlatego że klient Cię poprosił o funkcjonalnośc (np "jeśli nie ma zasobu, pokaż błąd") to ten test i implementacja powinny być do serwisu. Jeśli dodajesz kod tylko pod HTTP (bo np dostałeś błąd z Tomcata'a albo z konsoli przeglądarki, o którym user nie ma pojęcia) to powinno iść do testu i logiki kontrolera.
Tak powinno być w odpowiednio napisanej aplikacji, gdzie przestrzega się dependency inversion.
Niestety, większość (jak nie wszystkie) frameworków (Spring, laravel, django, railsy, większość z nich) bardzo lubi mieszać interfejs http z bazą, więc dadzą Ci do dyspozycji takie rozwiązania jak @Valid
w Spring (albo np Request
w Laravelu), którym bardzo łatwo można już w samym kontrolerze sprawdzić czy jakiś model ORM istnieje; tylko że to niestety niejawnie dodaje nam tight-coupling pomiędzy HTTP i bazą, niestety.
Więc tu się pojawia pytanie kolejne: Czy wolisz skorzystać z wygody (i tymczasowej szybkości) którą daje Ci spring tą adnotacją i zrobić to "bad and dirty"; czy wolisz napisać to w dwóch krokach (raz w kontrolerze, raz w logice z odpowiednimi testami), co zajmie trochę więcej czasu teraz - ale za to będzie jakby lepsze na długą metę. Decyzja należy do Ciebie (ja praktycznie zawsze staram się wybierać to drugie).
Więcej na ten temat możesz się dowiedzieć z tego filmiku The Principles of Clean Architecture.