
Simple Command
Kto kiedykolwiek pracował w projekcie railsowym napewno widział tzw. fat controllers, gdzie logika poszczególnych akcji wpakowana jest bezpośrednio w kontrolery, które automatycznie mają setki metod prywatnych i tysiące linii kodu, a akcje wyglądają np. tak:
Mam taką dobrą praktykę, że przenoszę wszelką logikę z kontrolerów do zewnętrznych domenowych klas serwisów, komend, use case’ów – zwał jak zwał. Testując takie kontrolery, obchodzi mnie tylko kształt odpowiedzi, które zwracają poszczególne akcje oraz to, czy wywoływany został odpowiedni serwis z odpowiednimi parametrami. To wszystko.
Aby zastosować takie rozwiązanie wystarczą PORO (Plain Old Ruby Object), natomiast ja lubię stosować małą, przyjemną bibliotekę Simple Command, która rozszerza nam nasze PORO o kilka rzeczy poprawiających wygodę i testowanie takich klas.
Link do libki: https://github.com/nebulab/simple_command
Z dokumentacji gema możemy wyczytać, że:
SimpleCommand
w swojej klasie należy (tu mam trochę problem z odpowiednim przetłumaczeniem tego na polski), poprzedzić ją właśnie klasą SimpleCommand
Jeśli chcesz wiedzieć, o co chodzi z prepend
zajrzyj tutaj.
.call
success?
i failure?
call
będzie dostępna pod atrybutem result
errors
Testowanie takiej klasy jest bardzo proste. Testujemy tak naprawdę tylko 3 przypadki:
Oczywiście, możemy iść głębiej i sprawdzać, czy wewnątrz komendy zostały wywołane jakieś operacje na innych klasach albo czy tablica errors
zawiera informacje o konkretnych błędach lub też, czy komenda rzuca wyjątki odpowiednie dla naszych przypadków testowych. Natomiast te trzy powyższe punkty wystarczają, by czuć się komfortowo z implementacją logiki zawartej w komendzie.
Przejdźmy teraz do konkretnego przypadku logowania użytkownika do API. Kod, który przygotowałem jest framework agnostic, to pokazuje, że SimpleCommand i wzorzec komendy, który wspiera ta biblioteka, można zastosować także poza Ruby on Rails, w skryptach, aplikacjach konsolowych, automatyzacjach itd.
Klasyczny przykład wg. rails waya:
Metoda login
jest dość długa. Jest parę ifów, jakieś złożone warunki, kilka renderów. Co, jeśli do aplikacji dodamy np. 2 Factor Authentication? Trzeba będzie tę logikę zawrzeć gdzieś między tymi ifami. Wydzielanie tego kodu do metod prywatnych nie do końca zda egzamin. Co, jeśli inna akcja będzie korzystać z tej samej metody, a w międzyczasie zmodyfikujemy metodę do nowych wymagań?
Przenieśmy całą logikę do komendy LoginUser
. Na potrzeby przykładu klasy User
i AuthToken
są zaślepkami, ich implementacja nas nie interesuje.
Co tu mamy? Komenda przyjmuje parametry potrzebne do zalogowania. W trakcie egzekwowania logiki rzuca własne wyjątki, łapie te wyjątki i ustawia adekwatnie do nich kody i odpowiedzi błędów. Jest kilka metod prywatnych, wydzielonych dla większej przejrzystości kodu.
Teraz zobaczmy na test takiej komendy:
Testy są bardzo proste. Najpierw sprawdzamy happy path, a potem to, jak komenda zachowuje się w przypadku niepoprawnych danych logowania. Tylko tyle i aż tyle.
A tak będzie wyglądało wywołanie komendy w kontrolerze i test dla akcji logowania:
A tutaj test:
Prosty scenariusz. Na poziomie kontrolera sprawdzamy tylko poprawność wywołania komendy z odpowiednimi parametrami. A odpowiedzialność za test logiki i jej poprawność jest wydelegowana do innej warstwy.
No, ale co teraz? W momencie, kiedy będziemy mieć więcej takich akcji, które korzystają z komend, czeka nas niezła ifologia. Możemy sobie to ogarnąć przez pomocnicze metody. Na potrzeby przykładu wrzuciłem je do includowanego modułu CommandHelpers. Komenda jest przechwytywana przez handle i on już odpowiada za prawidłowe obsłużenie jej odpowiedzi lub błędów. Zawsze ten handler można bardziej rozbudować i dostosować. Przedstawiam Ci natomiast minimalną wersję implementacji.
Potem w kontrolerze wystarczy coś takiego:
I już 😀
W efekcie mamy enkapsulację logiki w przyjemnej klasie zawierającej dodatkowo informacje o powodzeniu lub przerwaniu akcji oraz czyste i przejrzyste kontrolery.
Uwaga! W repozytorium SimpleCommand na Githubie może Ci się rzucić w oczy, że nie ma tam żadnych nowych commitów od kilku lat. Biblioteka wygląda na nieutrzymywaną, a więc możesz mieć obawy przed jej użyciem. Nie martw się. Wystarczy zajrzeć w kod gema i zobaczyć, że cały SimpleCommand to tak naprawdę czyste PORO bez żadnych zależności. Równie dobrze zamiast zainstalowania gemu możemy sobie ten kod zawrzeć w jakiejś pomocniczej klasie w aplikacji railsowej.