Testy jednostkowe
[info] Jeśli pracujesz na laboratorium, aby rozpocząć realizację kolejnego modułu, musisz wykonać poniższe operacje
sklonuj repozytorium ze swoim kodem (
git clone ŚCIEŻKA_DO_REPOZYTORIUM),zainstaluj potrzebne gemy (
bundle install --path vendor/bundle),dokonaj migracji bazy danych (
rails db:migrate)uruchom serwer (
rails server)
Jednym z najpopularniejszych w ostatnich latach spojrzeń na tworzenie oprogramowania, wykorzystywanym głównie w tak zwanych metodykach zwinnych, jest technika TDD (Test-driven development). W największym możliwym uproszeniu polega ona na przygotowaniu, jeszcze przed właściwą implementacją programu, kolekcji funkcji weryfikujących poprawność tworzonego w projekcie kodu. Stanowi ona sposób na zapisanie specyfikacji oprogramowania w sposób pozwalający na łatwe wyłapywanie błędów, nieuniknionych podczas programowania.
W wypadku naszego projektu, właściwy kod już powstał, ale nic nie stoi na przeszkodzie, abyśmy na przyszłość nauczyli się w jaki sposób przygotowywać testy jednostkowe.
Podejdziemy do nich w odrobinę inny sposób, niż klasycznie stosowany przy aplikacjach internetowych. Ze względu na to, że powstająca aplikacja serwerowa służy głównie jako interfejs programistyczny dla aplikacji mobilnej, nie będziemy testować wyników operacji na modelach, czy kontrolerach projektu, a wprost wyniki wysyłanych do serwera zapytań.
Aktualizacja projektu
Do zaprogramowania testów jednostkowych wykorzystamy bibliotekę Rspec. Na początek dodajmy odpowiedni gem (rspec-rails) do grup development i test pliku Gemfile.
group :development, :test do
...
gem 'rspec-rails'
endW następnym kroku aktualizujemy gemset aplikacji.
bundle install
Na koniec przygotowań, utworzymy pliki konfiguracyjne testów. W tym celu posłużymy się komendą:
rails generate rspec:install
Po jej zakończeniu w katalogu głównym projektu pojawi się katalog spec, będący miejscem, w którym znajdą się napisane przez nas testy.
Uruchamianie testów
Aby uruchomić wszystkie zaprogramowane testy jednostkowe, używamy komendy:
bundle exec rspec spec
Jeśli wywołamy je w tej chwili, powinniśmy uzyskać wynik podobny do poniższego, mówiący o braku jakichkolwiek przykładów.
Skoro nie zrobiliśmy jeszcze nic ponad instalację Rspec, wynik taki jest zupełnie logiczny.
Przygotowanie danych na potrzeby testów
Na potrzeby testów przydadzą nam się przykładowe dane. W związku z tym, zanim przystąpimy do pisania testów, przygotujemy prosty helper (RequestHelper), wywoływany przed rozpoczęciem każdego bloku testów.
W wygenerowanym przy instalacji Rspec katalogu spec tworzymy nowy plik z modułem helpera — requests_helper.rb.
Rozpoczyna się on od importu głównego helpera testów (rails_helper) i deklaracji modułu RequestHelper. Dalej definiujemy metodę klasy prepare_requests. W samej metodzie, na początek, usuwamy wszystkie istniejące w bazie obiekty, aby później utworzyć i zapisać w zmiennych po jednym przykładzie dla każdego modelu. Na koniec tworzymy i zwracamy słownik utworzonych przykładów.
Na koniec przygotowań edytujemy plik spec_helper.rb, aby uwzględniać nowy moduł w przetwarzaniu. Na początek importujemy utworzony przed chwilą plik request_helper, a zaraz po rozpoczęciu pętli konfiguracji dodajemy do niej zaimplementowany helper.
Prosty test
Po krótkich przygotowaniach możemy nareszcie przystąpić do pisania testów. Każdy z nich operować będzie na zapytaniach, w związku z czym, dla porządku, w katalogu spec zakładamy podkatalog requests. W nim z kolei utworzymy nasz pierwszy zbiór testów, zajmujący się weryfikacją poprawności API pod kątem zarządzania sesją — sessions_spec.rb.
[info] Nazwa pliku jest istotna!
Musimy pamiętać o tym, aby każdy zadeklarowany zbiór testów kończył się łańcuchem
_spec.rb. W innym wypadku Rspec nie wykryje go i testy nie zostaną uruchomione.
Implementację pliku sessions_spec.rb rozpoczynamy od zaimportowania helpera pozwalającego na wykorzystanie klas napisanej przez nas aplikacji (rails_helper). Następnie (Rspec.describe) deklarujemy zbiór testów Session management, realizujący testy zapytań (:type => :request).
W obrębie zbioru zadeklarujemy pierwszy test. Nie będzie on mieć jeszcze większego sensu ale pozwoli nam na zrozumienie zasady działania.
Po uruchomieniu testów (bundle exec rspec spec) uzyskamy już informację o przeprowadzeniu jednego testu:
Wyświetlane w pierwszej linii kropki symbolizują testy zakończone sukcesem. Trudno, aby nasz test zwracał błąd, kiedy jeszcze niczego nie weryfikuje. Dodajmy więc logikę do testu. Tworzymy zmienną value i przypisujemy do niej łańcuch something. Następnie oczekujemy (expect()) aby równał się on wartości something else (.to eq('something else')).
[info] Czym jest funkcja
expect()Funkcja ta to tak zwany matcher i działa podobnie do znanej Ci zapewne z języka C++ funkcji
assert(). Szczęśliwie jest znacznie bardziej wysokopoziomowa i pozwala na łatwe deklarowanie naszych oczekiwań wobec systemu. Przykładowo tego, aby zawartość zmiennej była równa jakiejś wartości, zapisany w niej słownik zawierał określony zbiór kluczy czy zapisany w niej łańcuch posiadał określony podłańcuch. Więcej o niej możesz przeczytać w dokumentacji Rspec.
Wiedząc, że łańcuch something różni się od łańcucha something else, możemy oczekiwać, że test zwróci błąd. Po uruchomieniu testów, faktycznie tak się dzieje.
Rspec informuje nas o nieudanym teście, wyjaśniając dokładnie przyczyny pomyłki. Poprawmy kod testu tak, aby był spełniany.
Po poprawce, uruchomienie testów nie zwraca już błędów.
Testy zapytań
Skoro znamy już podstawową zasadę przeprowadzania testów, spróbujmy przetestować któreś z zapytań do API. Przed uruchomieniem testów skorzystamy jeszcze z przygotowanego helpera. Uzupełnijmy plik sessions_spec.rb. Dodajemy do niego metodę before, działającą podobnie do tej, z którą spotkaliśmy się już w kontrolerach. Przed uruchomieniem każdego testu (:each) zapiszemy do zmiennej objects słownik z obiektami utworzonymi w metodzie prepare_requests(), aby później przypisać jego elementy do obiektów, które przydadzą nam się w testach. W wypadku testowania obsługi sesji, wystarczy nam obiekt użytkownika — studenta (@student).
Zmienna studenta pozwoli nam na przygotowanie danych. Naszym pierwszym właściwym testem będzie weryfikacja tego, czy przy podaniu błędnego hasła, system nie pozwoli nam na uzyskanie tokena. Zadeklarujmy zatem test.
Aby uzyskać token, zgodnie z tym, co możemy przeczytać w napisanej przez nas w poprzednim punkcie tutoriala dokumentacji, należy na ścieżkę /login wysłać metodą post zapytanie zawierające ciało formularza logowania. Formularz składa się z elementu głównego session, będącego słownikiem posiadającym pola index i password. Ponadto, aby zasymulować wysyłanie zapytania z poziomu interfejsu programistycznego, a nie przeglądarki, ustawiamy nagłówek Accept na wartość application/json.
[info] Testy powodujące błędy serwera
Test zakończy się porażką, jeśli zapytanie spowoduje błąd serwera, nawet jeśli ani razu nie użyliśmy w nim funkcji
expect().
Jeśli poprawnie wysłaliśmy zapytanie, test powinien przejść bez błędów. Nadal jednak niczego jeszcze nie przetestowaliśmy.
Odpowiedź serwera na zapytanie zapisuje się w zmiennej response, a konkretnie w jej polu body. Kod HTTP odpowiedzi znajdziemy w jej polu status. Spróbujmy podczas przeprowadzania testu wyświetlić ciało odpowiedzi.
Po jego uruchomieniu zobaczymy poniższy wynik.
Jak możemy zauważyć, uzyskaną odpowiedzią jest łańcuch JSON, który musimy zweryfikować w teście. Uzupełniamy go więc o zmienną z wartością oczekiwaną, która będzie słownikiem tożsamym z komunikatem błędu.
A na koniec, przy użyciu funkcji expect(), oczekujemy, aby uzyskana na nasze zapytanie odpowiedź, zgadzała się z nią.
Test powinien zakończyć się powodzeniem. Jakakolwiek zmiana w wartości oczekiwanej powinna wygenerować błąd.
Test poprawności uzyskanego tokena
Potrafimy już robić proste testy jednostkowe. Dla utrwalenia wiedzy, przyjrzyjmy się gotowemu testowi poprawności uzyskanego tokena:
Wysyłamy zapytanie w podobny sposób, co poprzednio, tym razem jednak podając poprawne hasło. Po wysłaniu zapytania, przeładowujemy obiekt @student, ponieważ poprawne przeprowadzenie autentykacji powinno uaktualnić jego token. Tworzymy wartość oczekiwaną z pobranym z przeładowanego studenta tokenem i weryfikujemy czy w odpowiedzi serwera znajduje się zgodne z nią ciało.
Test poprawności wylogowywania
Niektóre zapytania, jak na przykład wylogowywanie (delete /logout) wymagają autentykacji przy użyciu tokena. Aby nauczyć się je poprawnie wysyłać, przeanalizujmy jeszcze gotowy test poprawności wylogowywania:
Przez wylogowanie (deautentykację) rozumiemy usunięcie (przypisanie wartości nil) tokena z obiektu użytkownika. W związku z tym na początku testu oczekujemy, że token istnieje, czyli nie jest nil-em (expect(@student.token).not_to be_nil). Następnie wysyłamy zapytanie metodą delete. Istotne tutaj jest dodanie nagłówka Authorization o treści Token ŁAŃCUCH_TOKENA (pamiętając o wielkiej literze w słowie Token).
Po wysłaniu zapytania przeładowujemy obiekt studenta i upewniamy się, że do tokena został przypisany nil.
Przykłady pozostałych testów
Pełen zestaw testów jednostkowych dla naszej aplikacji znajduje się w aktualnej wersji repozytorium:
[info] Aktualny kod
Na koniec każdego modułu znajduje się łącze do pełnej wersji kodu, który powinien być jego wynikiem.
Last updated