Wróć do strony głównej
Cypress

Cypress – wprowadzenie (cz. 1)

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.

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
    • Mocha – bogaty w funkcje, JavaScriptowy framework do testowania
    • Chai – biblioteka asercji BDD/TDD
    • Sinon – biblioteka do mockowania dla JavaScript
  • 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.

Obsługiwane przeglądarki

Struktura katalogów

Po dodaniu do projektu Cypresss automatycznie wygeneruje nam następującą strukturę folderów: 

  • Fixturessą 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. 


    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.
  • Integration w tym katalogu znajdują się pliki testowe.Testy mogą mieć następujące rozszerzenia:
    • .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.
  • 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

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).

Pierwsze co ujrzymy po włączeniu interfejsu to zakładka Tests zawierająca listę testów z wyszukiwarką. Na tym etapie mamy również możliwość wyboru przeglądarki na jakiej będziemy przeprowadzać debugowanie. 

Po wybraniu dowolnego pliku z testami Cypress automatycznie uruchomi okno wybranej przeglądarki oraz przejdzie do wykonywania testów.

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 🙂 

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:

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:

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.



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ą:

Po tym zabiegu nasz nowo napisany test do buttona logowania wygląda następująco:

 

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ć:

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/

 

O autorze

Adrian Zaorski

Filantrop, wirtuoz Angulara i spawania nietopliwą elektrodą w osłonie argonu. W wolnych chwilach leży w wannie.

Chcesz razem z nami przygotowywać treści na bloga?
Dołącz do nas i twórz wartościowy content dla sympatyków Angulara z Angular.love!

Jeden komentarz

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *