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.
Spis treści
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 Given, When, Then, np.:
1 2 3 4 5 6 7 8 9 10 |
Scenario Outline: A user filters data Given Data '<dataName>' with property '<propertyName>' is added And User goes to the main page When User filters by property '<propertyName>' Then Only data '<dataName>' is visible Examples: | dataName | propertyName | | FOO_DATA | XYZ | | BAR_DATA | ABC | |
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:
1 2 3 |
When(/^User filters by property '([^']*)'$/, async (propertyName: string) => { await mainPage.setPropertyFilter(propertyName); }); |
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:
- Znajdź na stronie element X
- 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:
1 |
const filterElement = element(by.id('filter')); |
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:
1 2 |
const parentElement = element(by.partialLinkText('Filter')); const filterElement = parentElement.element(by.xpath('..')); |
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.log
nie 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:
1 2 3 4 5 6 |
const allElements = element.all(by.className('A')); const selectedElements = allElements.filter(function(element, index) { return element.getAttribute('class').then(function(className) { return !className.includes('B'); }); }); |
Pomijam przy tym fakt, że można to zrobić znacznie prościej:
1 |
const selectedElements = element.all(by.css('.A:not(.B)')); |
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
.
1 2 3 4 5 |
const penultimateElement = selectedElements.get( selectedElements.count().then(function(numberOfElements) { return numberOfElements - 2; }) ); |
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 reduce
jest 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.