Testy frontendu okiem początkującego

Pracując dotychczas glównie jako backend developer, z rozwijaniem frontendu nie miałem przesadnie dużej styczności. Sam zresztą nieco stroniłem od niego, mając do JavaScriptu i wszelkich jego frameworków stosunek wybitnie negatywny.

W tym roku postanowiłem to zmienić i przełamać się – w pracy częściej zacząłem brać na siebie taski wymagające zmian we frontendzie (starając się przy tym nie wykonywać ich tylko na zasadzie analogii do istniejących już funkcjonalności), a ostatnio także miałem okazję popisać trochę testów automatycznych.

Z tej właśnie okazji publikuję dziś kilka swoich przemyśleń na ten temat i rad, które początkujący może dać innemu początkującemu.

Stack technologiczny

Projekt do którego tworzyłem testy wykorzystywał na frontendzie Angulara 6. Oczywiście kod był dość dobrze pokryty testami jednostkowymi, jednak w miarę rozwoju aplikacji i związanych z nią procesów CI/CD, pojawiła się też konieczność przygotowania testów end-to-end (e2e), czyli takich, które są wykonywane z perspektywy użytkownika programu, podejmującego typowe dla niego interakcje – w tym przypadku obejmujące przede wszystkim poruszanie się po stronie internetowej.

Do pisania zautomatyzowanych testów e2e został wybrany framework Protractor (dedykowany do współpracy z Angularem). Pod spodem używa on biblioteki Selenium WebDriver, więc sposób wykonywania testów nie różni się diametralnie od tego w Selenium. Zdecydowaliśmy się też na użycie narzędzia o nazwie Cucumber (z ang. ogórek).

Zielony i marynowany

Tworzy ono znakomity duet z Protractorem, pozwalając na opisowe definiowanie testów przy wykorzystaniu języka o nazwie Gherkin (z ang. korniszon). Sam scenariusz pisany jest w oparciu o kilka słów kluczowych, takich jak GivenWhenThen, np.:

Tak przygotowany scenariusz testu z łatwością może zostać zweryfikowany przez osoby reprezentujące stronę biznesową, dzięki czemu możemy zyskać na przykład nowy sposób reprezentacji wymagań projektu. Wszystkie kroki zawarte w scenariuszu musimy oczywiście zaimplementować w wybranym języku programowania (Cucumber wspiera m.in. Javę, Ruby-ego, Kotlina oraz JS). My używaliśmy do tego TypeScripta, w którym definicja pojedynczego kroku może wyglądać tak:

Oczywiście metodę taką jak setPropertyFilter również musimy zaimplementować, tyle że w innym miejscu kodu. Jak jedak poradzić sobie przy owej implementacji, jeśli z frontendem ani TypeScriptem nie ma się zbyt dużo doświadczenia? Poniżej przedstawiam trzy rady, których znajomość uważam za istotną przy stawianiu pierwszych kroków z testami e2e implementowanymi w wymienionych technologiach.

XPath

Podejmując w testach interakcję z frontendową częścią systemu stajemy zazwyczaj przed zadaniem, które można opisać w dwóch punktach:

  1. Znajdź na stronie element X
  2. Kliknij w niego LUB sprawdź jego wartość

O ile drugi etap jest trywialny, o tyle znalezienie potrzebnego elementu nie zawsze jest oczywiste. Jeśli w kodzie zastosowano identyfikatory tam gdzie trzeba, to zadanie staje się banalne:

Nieco gorzej jest jeśli trzeba znaleźć jakiś inny sposób na dostanie się do upragnionego elementu, klucząc w gąszczu CSS-owych klas i właściwości. Wtedy znajomość języka XPath może okazać się nadzwyczaj przydatna.

Czym właściwie jest XPath? To język pozwalający na adresowanie elementów wchodzących w skład dokumentów XML. Dzięki niemu możemy pobrać obiekt, do którego trudno dobrać się w bardziej elegancki sposób. Jeżeli np. jesteśmy w stanie znaleźć element podrzędny względem poszukiwanego, możemy użyć takiego kodu:

Debugowanie

Tu będzie krótko – w razie problemów z implementowanymi testami warto zapoznać się z tematem debugowania w Protractorze. Przy moim pierwszym podejściu do naprawiania niedziałających testów nie tylko nie sięgnąłem po możliwości jakie daje debugger, ale też błędnie założyłem, że console.lognie będzie drukował wiadomości na terminal, w którym odpalałem testy i od razu zająłem się poszukiwaniem alternatywnych sposobów na printowanie logów.

Asynchroniczność

Ostatni, ale na pewno nie najmniej ważny temat, z którym warto się zaprzyjaźnić, to wszechobecna we współczesnych technologiach frontendowych asynchroniczność. Pisząc testy, z pewnością nieraz spotkamy się z obietnicami (promises), których posiadanie nie jest równoważne z posiadaniem docelowej wartości.

Aby swobodnie poruszać się w typescriptowym środowisku, zrozumienie tych zagadnień jest naprawdę istotne. Inaczej skazujemy się na tracenie czasu za każdym razem, gdy przyjdzie nam stworzyć jakiś odrobinę mniej banalny fragment kodu. Pamiętam, że gdy chciałem znaleźć elementy, które należą do klasy A, ale nie należą do klasy B, to musiałem się trochę nakombinować. Zrealizowałem to przy użyciu poniższego kodu:

Pomijam przy tym fakt, że można to zrobić znacznie prościej:

o czym też dowiedziałem się dopiero przy review mojego kodu. 😉

Okazuje się, że nawet zwykłe count() nie zwraca nam liczby elementów na liście, ale obietnicę. Chcąc zatem znaleźć przedostatni element kolekcji, znów trzeba sięgnąć po then.

Jeśli nie mieliście wcześniej do czynienia z programowaniem funkcyjnym, to i w tej kwestii warto zaopatrzyć się w podstawową wiedzę. Umiejętność posługiwania się funkcjami map, filter i reducejest raczej nieodzowna.

Podsumowanie

Reasumując, powiedziałbym, że pisanie takich testów to całkiem relaksujące zajęcie, choć jeśli testowany kod nie jest najlepszej jakości, to szukając poszczególnych elementów czasami trzeba się nieco natrudzić.  Z kolei jeżeli w projekcie istnieje już sporo gotowych i działających scenariuszy testowych oraz kroków do nich, to implementacja kolejnych testów sprowadza się już natomiast prawie tylko do pisania scenariuszy w Gherkinie.

Linki