Kurs AngularJS #16 – Testy

Kod JavaScript przeważnie tworzy się szybko, lekko i przyjemnie. Z takiej przynajmniej zasady wychodzimy. Jeśli chcemy jednak, żeby był on również napisany prawidłowo i zawsze spełniał swoje zadanie każdy napisany kawałek kodu powinien być solidnie przetestowany. Angular dostarcza nam ku temu narzędzia i sprawia, że całe zagadnienie staje się trochę łatwiejsze.

Testy jednostkowe

Testy jednostkowe polegają na testowaniu pojedynczych, odseparowanych od reszty kodu kawałków. Pomagają one ustalić czy logika takiego kodu jest napisana tak jak należy oraz czy chociażby funkcja działa zgodnie ze wszystkimi założeniami.

Przeprowadzenie takiego testu może być trudniejsze niż się wydaje ze względu na prosty fakt, iż przeważnie staramy się izolować nasz kod zależnie od roli jaką spełnia ale nie do takiego poziomu jakiego wymaga taki test. Powiedzmy, że nasz kod wyświetla posegregowaną listę pracowników względem ich imion – alfabetycznie. Bardzo prosty przykład. Ale ta lista musi zostać zaciągnięta dzięki zapytaniu XHR, posortowana i wyświetlona więc dochodzi manipulacja DOM. Żeby wyizolować sam fakt sortowania, w tym przypadku, Angular stara się trochę ułatwić nam to izolowanie. Symuluje więc zapytanie XHR i operacje na DOM tak że możemy przetestować samą logikę naszej funkcjonalności na czym własnie nam zależy.

Sam Angular stworzony jest z myślą o byciu przyjaznym do testowania ale wymaga to również pewnego obycia od nas. Takiego jak chociażby, rzeczywiste rozdzielanie kodu który pełni różne role w naszej aplikacji oraz stworzenie zależności między sobą dzięki DI co przy testach może być zasymulowane.

Narzędzia do testów

Karma – konsolowe narzędzie JavaScript które pozwala na ładowanie się treści naszej aplikacji i wykonanie testów. Możemy je tak skonfigurować, żeby imitowało zachowanie wielu przeglądarek, mamy wtedy pewność, że nasz kod działa wszędzie tak samo poprawnie. Jest wykonywane z konsoli i tam również wyświetla wyniki swojego działania. Jest oparte na node.js dlatego też powinno zostać zainstalowane poprzez npm / yarn.

Jasmine – framework testujący zachowanie naszego kodu w JS który stał się najpopularniejszym wyborem jeśli chodzi o pisanie testów pod Angulara. Dostarcza również narzędzia które pozwalają na odpowiednie ustrukturyzowanie naszych testów i tworzenie pewnych założeń odnośnie działania naszego kodu. Kiedy nasza aplikacja rozrasta się a wraz z nią ilość testów jaką trzeba wykonać, trzymanie ich w konwencji odpowiedniej struktury oraz dobra dokumentacja są kluczowe, a Jasmine znacznie nam w tym pomaga. Dostarcza bardzo dużą liczbę funkcji które sprawdzają wyniki działania względem poczynionych założeń. Zapewne napisze o niej osobny artykuł w najbliższej przyszłości.

Jeśli chcemy używać Karma z Jasmine powinniśmy używać narzędzia karma-jasmine.

angular-mocks – Angular dostarcza również moduł ngMock który dostarcza możliwość imitowania zachowań naszych zależności co pozwala na prawdziwe wyizolowanie testowanego przez nas kodu. Pozwala również na utrzymanie naszych testów synchronicznymi co sprawia, że są bardziej przejrzyste i łatwiej się z nimi pracuje. Jedną z najużyteczniejszych rzeczy które dostarcza nam ten moduł jest $httpBackend – pozwala nam na symulacje zapytań XHR w naszych testach i zwraca zamiast tego przykładowe dane.

Testowanie kontrolera

W Angularze nasza logika która znajduje się w kontrolerze jest odseparowana od widoku który znajduje się w osobnym pliku co sprawia, że sam kontroler jest łatwiejszy do przetestowania. Pierwszym przykładem który przetestujemy będzie funkcja strenght() która na podstawie długości naszego hasła będzie dostarczała informacje o jego sile.

Ponieważ kontrolery nie są widoczne globalnie potrzebujemy użyć angular.mock.inject żeby dostać się do naszego kontrolera. Pierwszym krokiem jest użycie funkcji module() dostarczonej przez angular-mocks. Ładuje ona przekazany do niej moduł co sprawia, że jest on dostępny do testów. Tę funkcję przekazujemy z kolei do funkcji beforeEach() dostarczonej przez Jasmine która zapewnia przed każdym testem że będziemy mieli dostęp do kodu. Później możemy użyć inject() w celu pozyskania dostępu do kontrolera.

Zauważ, że testy pisane są bardzo opisowo i “po ludzku”. Jeśli ktoś zagląda w kod po raz pierwszy to często bardzo prawdopodobnym jest, że po teście jakiegoś kawałka kodu dowie się szybciej do czego służy niż po samym kodzie. Zagnieżdżone w describe() odwołania są bardzo opisowe i dokładnie informują nas o tym co jest akurat testowane. Samo describe natomiast opisuje element który jest testowany. Składnia tych testów jest o tyle prosta, że chciałbym żebyś na chwilę obecną zadowolił się takim ogólnym opisaniem co jest do czego i co robi a do jej dokładniejszej analizy przejdziemy w artykule poświęconym tylko Jasmine.

Kiedy mamy już gotowy kod aplikacji i testu, chciałbym Ci pokazać jak to wszystko uruchomić. Tak żebyś mógł na bieżąco widzieć efekt działania swoich testów kiedy będziemy pisać kolejne.

Stwórz więc najpierw nowy katalog a następnie utwórz w nim folder app/ i plik index.html. W folderze app/ stwórz plik app.module.js i folder shared/ a w nim passwordCheck/. W passwordCheck/ utwórz pliki passwordCheck.controller.js i passwordCheck.controller.spec.js i uzupełnij je następującą treścią:

Plik app.module.js uzupełnij następująco:

A na koniec dodaj treść do pliku index.html.

Jak widzisz plik Angulara znajduje się w folderze bower_components bo to właśnie przez narzędzie Bower pobierzemy nasze front-endowe zależności. Wejdź więc przez konsole do głównego folderu aplikacji i wpisz:

Teraz musisz zainstalować Angulara oraz angular-mocks. Zrobisz to dzięki pomniższym komendom:

Jeśli chodzi o zależności front-endowe to by było na tyle. Twoja strona powinna się odpalać i nie wyrzucać w konsoli żadnych błędów związanych z faktem, że mogłoby jej czegoś brakować. Czas więc zainstalować narzędzia do testowania. To zrobimy przez npm o którym już słyszałeś przy okazji kursu JS. Wpisz następujące komendy:

Teraz mamy zainstalowane całe środowisko do pracy. Czas więc na jego konfiguracje i przetestowanie. Żeby skonfigurować Karme musimy wpisać w konsoli:

Przeskakujemy wszystkie pytania naciskając cały czas enter i powinniśmy dostać informację, że wszystko zostało skonfigurowane poprawnie a w naszym bazowym katalogu pojawi się plik karma.conf.js. Otwieramy go i odpowiednio uzupełniamy:

Zauważ, że używamy dwóch przeglądarek do testów i ściągnęliśmy wcześniej do nich odpowiednie moduły. Upewnij się, że masz zainstalowane obie ew. zostaw jedną którą masz. Teraz możesz uruchomić już rzeczywisty test poprzez wpisanie komendy:

Wszystko powinno być w porządku i nasz kod powinien przejść test na 100%. Jeśli jednak coś pójdzie nie tak, śmiało napisz do mnie a pomogę Ci w konfiguracji. Jak widzisz Karma działa w tle i ma otwarte dwie przeglądarki do testów. Jeśli Ci one nie przeszkadzają możesz zostawić ją działającą z tymi dwiema przeglądarkami i rozwijać swoją aplikację. Zmiany będą uwzględniane na bieżąco i przy każdej zmianie zostanie przeprowadzony test czy nadal wszystko jest w porządku. Ja osobiście zamykam Karme i te okna bo mi najzwyczajniej przeszkadzają a testy uruchamiam kiedy skończę je pisać i skończę pracę nad jakąś określoną funkcjonalnością.

Teraz kiedy masz już środowisko i udało Ci się uruchomić swój pierwszy test jednostkowy czas przejść dalej.

Na początku dodajmy do naszego testu przetestowanie funkcjonalności pod względem innej liczby znaków niż tej powyżej 12. Kod ten dodajemy bezpośrednio pod testem (bloku it).

Jak widzisz dodałem dwa testy które sprawdzają dwie dodatkowe ewentualności. Jak widzisz niektóre rzeczy się tutaj powtarzają. Jest to stworzenie zmiennej $scope i zaciągnięcie kontrolera. Im więcej testów będziemy dodawać tym będzie gorzej. Mamy jednak funkcję beforeEach() do której możemy przenieść kod który musi zostać wykonany przed każdym testem i Jasmine zautomatyzuje dla nas ten proces.

Dzięki przesunięciu powtarzającego się kodu do bloku beforeEach każdy test zawiera jedynie ten kod który jest dla niego niezbędny a nie ten który jest ogólnie potrzebny dla każdego testu. Jasmine oczywiście zawiera więcej bloków tego typu ale o tym również opowiemy sobie w dedykowanym temu artykule.

Testowanie filtrów

Jak wiesz filtry przekształcają dane z kontrolera w bardziej przyjazne dla użytkownika, takie przynajmniej jest ich założenie. Są ważne z powodu przeniesienia na nie przekształcania danych z logiki aplikacji, uproszczając ją przy tym.

Napiszemy prosty filtr którego zadaniem będzie poinformowanie użytkownika o długości jakiegoś ciągu znaków.

Jak widzisz zachodzi konwersja podanego ciągu znaków na tekst poprzez połączenie go z tekstem co spowoduje automatyczne rzutowaniu typu. Jeśli natomiast tekst nie zostałby podany to zostanie zastąpiony pustym ciągiem znaków. Od powstałej w ten sposób wartości pobierana jest jej długość – tą wartość zwraca filtr jako wynik swojego działania. Teraz czas na test.

Jak widzisz niewiele się tutaj zmienia poza sposobem pozyskania filtra chociaż jest on bardzo podobny jak w przypadku kontrolera. Zauważ też, że i tutaj staramy się trzymać powtarzający się kod w odpowiednim bloku. Na chwilę obecną jest tylko jedna linijka ale może z czasem się rozrosnąć.

Testowanie dyrektyw

Dyrektywy w Angularze są odpowiedzialne za trzymanie w sobie skomplikowanych funkcjonalności które później są dostępne w postaci tagów bądź atrybutów w widoku. Pisanie testów dla dyrektyw jest bardzo ważne ze względu na fakt, że w aplikacji mogą być używane w wielu różnych kontekstach.

Zaczniemy od prostego przykładu bez żadnych zależności.

Dyrektywa jak widzisz jest banalnie prosta. Napiszmy więc do niej test.

Z nowości jakie pojawiły się w tym teście mamy $rootScope$compile. Będą one nam potrzebne, żeby skompilować naszą dyrektywę tak jakby miało to miejsce na stronie. Odpalenie $digest powoduje wykonanie się wyrażenia {{ 2 + 2}}. Po tym możemy już sprawdzić zawartość zmiennej element która posłużyła nam za pojemnik i sprawdzić czy zawiera w sobie treść którą powinna wyprodukować dyrektywa.

Testowanie dyrektyw które komunikują się ze sobą

Może pamiętasz, że przy okazji omawiania tematu tworzenia własnych dyrektyw poruszyliśmy również wątek tworzenia dyrektyw które komunikują się między sobą dzięki właściwości transclude. Był to dosyć skomplikowany przykład więc musimy również dokładnie wytłumaczyć sobie jak przebiega test takiej dyrektywy. Są one bowiem traktowane w specjalny sposób przez kompilator.

Przed wywołaniem funkcji kompilującej zawartość dyrektywy jest usuwana i dostarczana przez funkcję łączącą. Widok dyrektywy załączany jest dopiero później kiedy załadowana zostanie do niej treść która ma zostać dołączona. Proces ten przebiega mniej więcej tak:

Jeżeli dyrektywa używa połączenia na całym elemencie to cały element zostaje zastąpiony komentarzem a dopiero później pod tym komentarzem wstawiana jest gotowa treść.

Trzeba być tego świadomym kiedy piszemy testy dla dyrektyw które stosują takie połączenie. Jeśli bowiem przekażesz do kompilacji taką dyrektywę w elemencie nadrzędnym zamieni się na komentarz i stracisz do niej dostęp.

Żeby w prosty sposób sobie z tym poradzić po prostu upewnij się, że twój element jest otoczony powiedzmy jakimś <divem>.

Jeśli w swoich dyrektywach używasz templateUrl rozważ używanie karma-ng-html2js-preprocessor. Pozwoli to na wcześniejsze przekompilowanie HTMLi i uniknięcie ładowania ich oddzielnie podczas trwania testu. W innym przypadku mogą również wystąpić problemy związane z różnicami pomiędzy testowym katalogiem a katalogiem aplikacji.

Testowanie obietnic

Podczas testowania obietnic ważne jest żebyśmy wiedzieli, że ich rozwiązanie jest powiązane z cyklem $digest. Oznacza to, że metody then, catch, finally zostaną wywołane po przebiegu tego cyklu. W testach możesz to wywołać dzięki metodzie $apply twojego $scope. Jeśli jednak nie masz $scope to zawsze możesz wstrzyknąć do testu $rootScope i zastosować tę metodę na nim.

Serwis $q pozwala na odpalanie asynchronicznych funkcji i używanie zwracanych przez nie wartości kiedy już zapytanie zostanie obsłużone. Nim posłużymy się w celach poglądowych do testu.

Używanie beforeAll()

Funkcja Jasmine – beforeAll(), bywa użyteczna przy współdzieleniu ustawień testu w celu zmniejszenia czasu jego wykonywania czy też skupienia się w konkretnych testach na konkretnych funkcjonalnościach.

Domyślnie ngMock tworzy injector dla każdego testu żeby upewnić się, że na siebie nie wpływają. Jednakże jeśli użyjemy beforeAll()ngMock będzie musiał stworzyć injector jeszcze przed uruchomieniem jakiegokolwiek testu i współdzielić go pomiędzy wszystkimi testami w danym bloku describe. Będziemy tutaj mieli do czynienia z metodą module.sharedInjector(). Kiedy zostaje wywołana w bloku describe pojedynczy injector jest współdzielony pomiędzy testami w tym bloku.

W poniższym teście będziemy testować serwis któremu długo schodzi z wygenerowaniem odpowiedzi. Żeby uniknąć czekania używamy module.sharedInjector() beforeAll() żeby odpalić ten serwis tylko raz.

Testy end-to-end

Kiedy aplikacja przybiera na rozmiarach i na swojej złożoności, staje się wręcz niemożliwym, żeby manualne testy zweryfikowały jej poprawność i nowe funkcjonalności, wyłapały bugi i zgłosiły regresje. Testy jednostkowe są czymś co moglibyśmy nazwać pierwszą linią obrony. Czasem jednak problem leży w integracji pomiędzy elementami które tego typu testach nie mogą zostać wyłapane. Od tego właśnie są testy end-to-end (E2E, od końca do końca).

My będziemy używać Protractoraktóry wykonuje takie testy symulując zachowanie użytkownika co pomaga zweryfikować naszą aplikację. Jest to technologia stworzona w node.js, gdzie testy również piszemy w JS. Protractor używa Jasmine do obsługiwania składni więc nie musimy znowu uczyć się czegoś całkowicie nowego bo Jasmine już przynajmniej trochę znamy. Tak jak w przypadku Jasmine sam test składa się z kilku bloków it które opisują nasze wymagania odnośnie aplikacji. Składają się z komand i oczekiwań. Komendy mówią Protractorowi żeby zrobić określone rzeczy takie jak przewinięcie strony w dół czy kliknięcie na przycisk. Wymagania natomiast zapewniają go jaka powinna być np. wartość czegoś albo adres URL. Jeśli jakiekolwiek wymaganie w bloku it się nie sprawdzi obleje on test.

Test może zawierać również beforeEachafterEach wykonywane przed i po każdym teście niezależnie od tego czy będzie on poprawny czy też nie. Mogą również mieć funkcje pomocnicze które zapobiegną powtarzaniu się kodu w blokach it. Przeanalizujmy jeden prosty test: