Testy end-to-end to metodologia testowania aplikacji z perspektywy użytkownika. Ma to na celu zapewnienie, że aplikacja od początku do końca zachowuje się zgodnie z oczekiwaniami (od frontu po backend). W tej serii artykułów pokażemy wam jak pisać testy e2e dla aplikacji Angularowej z wykorzystaniem Cypressa.
Na wstępie chciałbym podziękować współautorowi – Mateusz Stefańczyk, który wspomógł mnie podczas pisania tego artykułu.
Także podziękowania należą się Norbertowi Pioterczakowi za zaktualizowanie artykułu i opisanie najnowszych wersji.
Seria artykułów
Z racji na obszerność tematu testów jak i samego Cypressa postanowiliśmy podzielić artykuł na kilka mniejszych części, żeby ułatwić wam zrozumienie tego tematu i oddzielić od siebie pewne elementy.
Spis części
- Wprowadzenie
krótki opis frameworka, instalacja, konfiguracja, Desktop GUI, proste testy interfejsu, najlepsze i najgorsze praktyki testowania UI - Testy integracyjne
fixtures, testy integracyjne, najlepsze i najgorsze praktyki testowania integracyjnego - Rozszerzenie testów e2e
custom commands, logowanie githubem jako social providerem - Czy potrzebny nam kolejny framework e2e?
cypress vs selenium, zastąpienie protractora
Czym jest Cypress
Cypress to framework (open source) służący do pisania testów integracyjnych oraz e2e. Charakteryzuje się dużą kompleksowością. Dostarcza nam wszystkie niezbędne narzędzia – pisanie testów możemy rozpocząć zaraz po instalacji. Nie wymaga od nas instalacji żadnych dodatkowych bibliotek. Poniżej przedstawiamy cechy, które wyróżniają go od innych dostępnych frameworków:
- nie opiera się na webdriver (w odróżnieniu do selenium, które korzysta z webdrivera, więcej w kolejnej części artykułu)
- all-in-one – Cypress zawiera w sobie środowisko testowe, biblioteki asercji oraz narzędzia do mockingu oraz stubbingu
- automatycznie oczekuje na (rozszerzenie tego punktu w dalszej części):
- załadowanie DOM, aż elementy zaczną być widoczne
- ukończenie animacji
- zakończenie wywołań XHR i AJAX.
- time-traveling – Cypress robi snapshot’y podczas wykonywania testów. Dzięki temu możemy prześledzić co wydarzyło się na każdym kroku naszego testu. Na wypadek wystąpienia błędu pozwala bardzo szybko zidentyfikować źródło np. zweryfikowanie czy został naciśnięty odpowiedni button itp.
- dostarcza wszystkie komendy pod jednym globalnym obiektem cy.W ten sposób pisanie testów jest intuicyjne i proste.
- pozwala na pisanie testów w JavaScript oraz TypeScript.
- funkcjonalne GUI, które umożliwia podglądanie wykonywanych testów w czasie rzeczywistym oraz debugowanie z wykorzystanieorazm DevTools’ów.
- automatycznie przeładowuje testy, podczas zmian w pliku testowym.
Dodawanie Cypressa do projektu
Aby dodać Cypressa do istniejącego projektu Angular, użyj komendy:
1 |
npm install cypress --save-dev |
Struktura katalogów
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/cypress /fixtures - example.json /integration /examples - actions.spec.js - aliasing.spec.js - assertions.spec.js - connectors.spec.js - cookies.spec.js - cypress_api.spec.js - files.spec.js - local_storage.spec.js - location.spec.js - misc.spec.js - navigation.spec.js - network_requests.spec.js - querying.spec.js - spies_stubs_clocks.spec.js - traversal.spec.js - utilities.spec.js - viewport.spec.js - waiting.spec.js - window.spec.js /plugins - index.js /support - commands.js - index.js |
- Fixtures – są używane jako statyczne dane w naszych testach. Najczęściej stosujemy je do jest mockowania zapytań sieciowych (xhr/ajax), gdzie wskazujemy, który fixture ma zostać użyty jako źródło danych dla odpowiedzi żądania serwerowego, np.
1server.route('GET', '**/example/**', 'fx:example.json')
Powyższa linijka spowoduje, że wszystkie zapytania typu GET, pod adres url zawierający w swoje ścieżce /example/, jako odpowiedź, zwrócą dane z pliku example.json znajdującego się w katalogu fixtures.
Fixtures można również pobrać przy użyciu komendy cy.fixture(). Służy ona do załadowania danych znajdujących się we wskazanym pliku. Pobranie danych w ten sposób jest pomocne przy dokonywaniu wszelkich asercji.- .js
- .jsx
- .coffe
- .cjsx
- Plugins– Inicjalnie w tym katalogu Cypress generuje nam plik index.js, który zawiera defaultowe pluginy. Plik uruchamiany jest przed każdym testem. Pluginy pozwalają nam na modyfikowanie oraz rozszerzanie wewnętrznego zachowania Cypressa.
Od wersji 10 Cypressa funkcjonalność została wycofana, ze względu na wprowadzenie obsługi plików konfiguracyjnych .js i .ts, które są obecnie wymagane. Została wycofana także konfiguracja w formacie JSON. Zastąpiono ją opcją konfiguracyjną ‘setupNodeEvents()’ i ‘devServer’. SetupNodeEvents() jest ekwiwalentem eksportowanego pliku plugins.js. Przez tę funkcję można również taki napisany wcześniej plik zaimportować, chociaż nie jest to zalecane z powodu braku typowania – użytecznego w przypadku testowania aplikacji Angular.
- Support – ten katalog jest idealnym miejscem do umieszczenia reużywalnych funkcji, zachowań jak własne komendy lub globalne konfiguracje dla naszych testów t.j mockowanie zapytań serwerowych.
Oprócz wyżej wskazanych katalogów, istnieją również foldery, które mogą zostać wygenerowane po uruchomieniu środowiska testowego, zawierające np. nagrania naszych testów, screenshoty itp. Warto rozważyć dodanie tego typu katalogów do pliku .gitignore
Wersja 11 nie tworzy domyślnie (po instalacji metodą wskazaną powyżej) struktury katalogów. Katalog wraz z przykładową konfiguracją pojawia się dopiero po uruchomieniu cypressa komendą ‘cypress open’ i wybraniu metody testowania (w tym wypadku dla uproszczenia e2e). Dopiero wtedy dostajemy informację jakie pliki i katalogi zostaną utworzone. Są to:
– cypress.config.ts – główny plik konfiguracyjny, w którym podajemy opcje konfiguracyjne jeżeli chcemy zmieniać domyślne zachowanie cypressa – podstawowa konfiguracja pozwala bez problemu uruchomić testy
– cypress/fixtures/example.json – przykładowy plik fixture
– cypress/support/commands.ts – przykładowy plik w którym docelowo będziemy dodawać własne komendy dostępne w ramach wrappera ‘cy’
– cypress/support/e2e.ts – przykładowy plik, który ładuje się przed testami e2e.
Po dodaniu przykładowego testu z poziomu GUI pojawia się nam dodatkowo katalog e2e, w którym będą znajdować się pliki spec.ts (domyślny format używany przy aplikacji Angular) opisujące poszczególne testy.
Wskazana powyżej struktura katalogów może być zmieniona i dowolnie konfigurowana.
Desktop GUI
Interfejs graficzny to aplikacja napisana w Electronie znacznie ułatwiająca tworzenie oraz debugowanie testów. Możemy ją odpalić w klasycznym projekcie poprzez cypress open
lub jeżeli korzystamy z nrwl/nx, desktop gui odpalamy poprzez ng e2e app-e2e
(podczas developmentu i debugowania warto dodać flagę --watch
).
Po uruchomieniu pokaże się nam okno, w którym możemy wybrać rodzaj testów. Obecnie do wyboru jest znana z poprzednich wersji opcja e2e, oraz nowość od wersji 10 czyli Component Testing. My skupimy się na tych pierwszych. Po kliknięciu w okno E2E Testing pierwszy raz wyskoczy nam informacja o niezbędnych plikach, które zostaną dodane (m.in plik konfiguracyjny).
Okno to widzimy jednorazowo, kolejnym razem przejdziemy już od razu do okna wyboru przeglądarki.
Tutaj mamy do wyboru zarówno popularne przeglądarki (Chrome, Firefox) jak i Electron. Po wybraniu przeglądarki możemy przejść już do listy testów. W przypadku gdy nie mamy jeszcze żadnych scenariuszy testowych Cypress zaproponuje nam stworzenie nowego, bądź skorzystanie z paczki przykładowych.
Na potrzeby artykułu użyjemy gotowej paczki nazwanej ‘Scaffold example specs’. Po kliknięciu odpowiedniego przycisku ukaże nam się lista przykładowych scenariuszy testowych.
Po wybraniu testu uruchomione zostanie dokładnie to, o co toczy się całe zamieszanie, czyli okno Cypressa, z rozpisanymi poszczególnymi testami. Postęp jak i wyniki będziemy mogli obserwować na otwartej podstronie.
Widok, przedstawiający wykonywane testy oferuje zdecydowanie więcej funkcjonalności. Mamy dostęp do pełnych logów z wykonywanych testów wraz ze szczegółami każdego z kroków. Co więcej po najechaniu na konkretny krok podgląd aplikacji wyświetli stan aplikacji/snapshot jaki był w momencie danego polecenia – świetne ułatwienie podczas debugowania.
Warty wspomnienia jest Selector Playground
, którym możemy w bardzo szybki sposób wygenerować selectory do elementów na aktualnym widoku, a ponadto – zawsze będą one zgodne z najlepszymi praktykami Cypressa.
Testy interfejsu
Na początku skupimy się na testowaniu samego interfejsu z pominięciem API (testy integracyjne w drugim artykule z serii). Możesz zacząć pisać testy w swojej aplikacji – jeśli nie masz skonfigurowanego Cypressa w swoim projekcie, to w przypadku braku nrwl/nx możesz skorzystać z schematicsa dostępnego tutaj. Prezentowane w artykule przykłady pochodzą z mini-aplikacji napisanej specjalnie na potrzeby przedstawienia testu (https://bit.ly/2WSNCsS) i to właśnie z niej polecamy korzystać przechodząc nasz poradnik 🙂
W repozytorium razem z przykładową aplikacją dostaniemy Cypressa w starszej wersji. Nic nie stoi na przeszkodzie, żeby spróbować po zaznajomieniu się z obsługą Cypressa dodać najnowszą wersję i zweryfikować swoją wiedzę 🙂
Załóżmy, że chcemy przetestować następujący widok z naszej aplikacji, zawierający prostą kartę z formularzem logowania:
Utworzymy plik signing.spec.ts
w katalogu e2e/src/integration
i napiszemy nasz pierwszy blok:
1 2 3 |
describe(“login form”, () => { // }); |
describe
to metoda Cypressa zawierająca jeden lub więcej powiązanych testów. Za każdym razem, gdy zaczynasz pisać nowy zestaw testów dla danej funkcjonalności robisz to w środku bloku describe
.
Kolejnym elementem układanki na który trafiamy jest metoda it
oraz docelowy kod przypadku testowego:
1 2 3 4 5 6 7 8 |
describe(“login form”, () => { it('show error when email is empty', () => { cy.visit('/'); cy.get(‘[data-test=”login-email-input”]’).click(); cy.get(‘[data-test=”'login-password-input”]’).click(); cy.get(‘[data-test=”login-email-error”]’).should('be.visible').contains('Email address is required'); }); }); |
it
pełni rolę wrappera dla konkretnych, pojedynczych testów. cy
odnosi się bezpośrednio do Cypressa – tylko w ten sposób możemy się dostać do metod, które oferuje.
cy.visit(...)
jest metodą, która wykona akcję przejścia na route podany jako argument (jest to równe wpisaniu url w przeglądarkę. W ten sposób również należy sobie wyobrażać testy Cypressa – niczym rozkazy dla normalnego użytkownika).
cy.get(...)
to metoda do wybierania elementów na stronie. Jako argument przyjmuje selector (np. ten, który wygenerujemy poprzez Selector Playground) i mówi cypressowi dosłownie, aby złapał element, który pasuje do selectora. Na obiekcie, który get
zwróci możemy wykonać wiele kolejnych metod jak np. should
przyjmujący argument definiujący wymaganie, które sprawdzamy (więcej o selektorach, których można użyć w cy.get(...)
przeczytasz w sekcji Best & bad practices)
Z tak napisanym testem zawsze będziemy mieli pewność, że w przypadku pozostawienia inputa email bez wartości zostanie wyświetlony błąd o konkretnej zawartości
Warto już na tym etapie wspomnieć o bardzo ważnej zasadzie, testy powinny działać indywidualnie – niezależnie od siebie. W praktyce oznacza to, że w naszym przypadku w każdym teście będziemy chcieli zrobić cy.visit('/')
– aby nie powtarzać tego kodu możemy użyjemy beforeEach
w bloku describe
.
1 2 3 |
beforeEach(() => { cy.visit('/'); }); |
Kolejną rzeczą wartą sprawdzenia jest to, czy button logowania jest disabled w momencie, gdy nasza forma jest invalid, oraz czy button jest enabled przy poprawnie zwalidowanej formie. Aby uprosić sobie testowanie stworzymy małego utilsa, którym skrócimy pisanie utila. W folderze e2e/src/support
tworzymy plik get-data-test.util.ts
z zawartością:
1 |
export const getDataTest = (dataTestId: string) => `[data-test=${dataTestId}]`; |
Po tym zabiegu nasz nowo napisany test do buttona logowania wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
describe('sign in button', () => { it('sign in button should be disabled when form is invalid', () => { cy.get(getDataTest('login-password-input')).click(); cy.get(getDataTest('login-email-input')).click(); cy.get(getDataTest('login-sing-in-button')).should('be.disabled'); }); it('sign in button should be enabled when form is valid', () => { cy.get(getDataTest('login-password-input')).type('test@email.com'); cy.get(getDataTest('login-email-input')).type('test@email.com'); cy.get(getDataTest('login-sing-in-button')).should('be.not.disabled'); }); }); |
Spróbuj samodzielnie wykonać podobny test w naszej aplikacji.
W interfejsie logowania po zaznaczeniu checkboxa “Remember me” powinien wyświetlać się napis lorem ipsum. Spróbuj sam napisać test do tej części logowania (będzie niemal identyczny jak pierwszy test dotyczący errora przy pustym inputcie emaila).
Po napisaniu przypadków testowych możemy w Desktop GUI sprawdzić ich wynik.
Widzimy krok po kroku jak każde z kolejnych wymagań zostało spełnione i testy zostały zakwalifikowane jako spełnione.
Przykładowe selectory
cy.get(‘input’)
– znajdź element z tagiem input
cy.get(‘.menu’)
– znajdź element z klasą .menu
cy.get(‘#menu’)
– znajdź element z id .menu
cy.get(‘a[href=”login]’)
– znajdź element a
z atrybutem href=”login”
cy.get('[data-test="sidebar')
– znajdź element z atrybutem data-test=”sidebar”
Automatyczne oczekiwanie
Tak jak zostało wspomniane w poprzednim akapicie – Cypress automatycznie oczekuje, aż dany element osiągnie stan jakiego oczekujemy. Teraz, gdy poznałeś podstawy pisania testów interfejsu łatwiej będzie nam zgłębić ten temat 🙂
W przypadku stawiania wymagań (ang. assertion) (np. .should(‘be.disabled’)
) Cypress automatycznie zaczeka przez określony czas, aż nasz element osiągnie oczekiwany stan, dzięki czemu nie musimy przejmować się precyzyjnym określeniem kiedy element powinien spełnić postawione wymaganie. Wspomniane oczekiwanie polega na ciągłych próbach sprawdzania postawionych oczekiwań (więcej tutaj)
Wiele funkcji ma wbudowane domyślne wymagania, przykładowo .get()
i .find()
oczekuje, że element będzie istnieć w DOM, .type()
oczekuje elementu umożliwiającego pisanie, a .click()
elementu z którym można podjąć interakcję. Powyższe wymagania są objęte automatycznym oczekiwaniem, przykładowo podczas korzystania z funkcji .click()
Cypress upewnia się, że z danym elementem można przeprowadzić interakcje. W przeciwnym wypadku zaczeka, aż element:
- nie będzie schowany (hidden),
- nie będzie zakryty (covered),
- nie będzie zablokowany (disabled),
- nie będzie w trakcie animacji.
To przekłada się na dobry developer experience oraz ogranicza ilość rzeczy, które musimy brać pod uwagę podczas pisania testów. Więcej na temat powyższych punktów można przeczytać w oficjalnej dokumentacji.
Best & bad practices
Selectowanie elementów HTML
dobrze: używanie atrybutu data-*
aby odizolować selectory od zmian CSS lub JS
źle: używanie w selectorach elementów, które mogą się zmienić
Praktycznie każdy test, który napiszecie będzie zawierał selectory elementów. Zastosowanie się do zasady użycia atrybutu data-*
pozwoli zaoszczędzić sporo potencjalnych problemów, które mogłyby wyniknąć np. podczas zmiany klasy CSS konkretnego elementu.
Jak najlepiej uniknąć tego problemu?
- nie selectuj elementów bazując na ich atrybutach CSS-owych (
id
,class
,tag
) - nie celuj w elementy poprzez
textContent
- używaj atrybutu
data-*
Jak to działa?
Załóżmy, że mamy taki button, który chcemy przetestować:
1 2 3 |
<button mat-button id="call-button" class="call-button" name="call-button" data-cy="call-button"> Call me </button> |
Możliwości odniesienia się do niego:
Selector | Kiedy używać |
cy.get('button') |
nigdy – brak kontekstu, mocno generyczny |
cy.get('.call-button') |
nigdy – przywiązany do styli, duża szansa zmiany |
cy.get('#call-button') |
rzadko – ale dalej związany ze stylowaniem |
cy.get('[name=call-button]') |
rzadko – narusza sementyczność HTML |
cy.contains('Call me') |
zależnie – bazuje na wartości |
cy.get('[data-cy=call-button') |
zawsze – wyizolowane od wszelkich zmian |
Text Content
Po obejrzeniu tabelki powyżej możesz czuć się lekko zdezorientowany i zadawać sobie pytanie:
> dlaczego w tabelce cy.contains
jest zielone, skoro wcześniej padła informacja „Nie celuj w elementy poprzez textContent
?
Odpowiedź jest prosta, ale nie oczywista. Czy chcesz, aby test nie przeszedł gdy zmieni się wartość elementu?
> Jeśli test powinien nie przejść, użyj cy.contains()
> Jeśli test powinien przejść, użyj data-*
attribute
Ciekawostka
Selector Playground automatycznie generuje/proponuje selectory zgodne z dobrymi praktykami.
Odwiedzanie zewnętrznych stron
dobrze: Testuj tylko to co kontrolujesz. Unikaj używania zewnętrznych serwerów/serwisów, natomiast jeżeli musisz, to zawsze używaj cy.request()
źle: testowanie/otwieranie stron lub serwerów, których nie kontrolujemy
Pierwszą rzeczą, którą robi wiele osób jest angażowanie zewnętrznego serwera w swoich testach. Mógłbyś tego chcieć w przypadku np.:
- testowania różnych auth providerów z OAuthem
- weryfikowania czy zmiany pojawiają się na zew. serwerze
- sprawdzenia, czy przyszedł e-mail wysłany przez “forgot password”
Najprawdopodobniej spróbowałbyś użyć cy.visit()
żeby poruszać się po innych stronach, jednakże nigdy nie powinieneś tego robić podczas testowania, ponieważ:
- jest to bardzo czasochłonne i spowalnia pisanie testów
- zewnętrzna strona może:
- zmieniać swoją zawartość
- mieć błędy, na które nie masz wpływu
- może wykryć, że używasz skryptu do testowania i zablokować dostęp (np. github tak robi)
- prowadzić kampanię A/B
- zabraniać takich akcji poprzez swój regulamin
Istnieje kilka strategii radzenia sobie w takich sytuacjach, które możesz zgłębić w naszym następnym artykule z serii o Cypressie (dostępny wkrótce).
Źródło: https://www.cypress.io/
Super artykuł! Bardzo fajnie ujmuje temat. Czekam na kolejne części. Na pewno polecę w naszej firmie testerom przeczytać artykuł. Pozdrawiam serdecznie
Świetny artykuł 🙂