Powrót do: Praktyczne wprowadzenie do testów automatycznych z Playwright
Najprostsza implementacja wzorca POM
Prezentacja
Dodatkowe materiały
Bazujemy na kodzie lekcji L08_dane_i_wtyczka_pw
Kod wynikowy tej lekcji znajduje się tu: L03_pom_login
Pamiętaj, aby po danej porcji pracy: uruchamiać test, commitować poprawnie działający kod 😉
Jeśli w poprzednich lekcjach go nie skonfigurowałeś tych narzędzi, to możesz to zrobić bazując na lekcjach bonusowych: Wersjonowanie projektu z Git oraz Prettier, czyli formatter kodu.
Page Object Model
Page Object Model może zostać zaimplementowany na różne sposoby i z różnym poziomem złożoności. Wszystko zależy od języka programowania, zastosowanych dodatkowych wzorców, wymagań i potrzeb. W tej lekcji zaczniemy od jednej z najprostszych implementacji 🙂
Page Object Model w najprostszej postaci
Page Object Model w jednej z najprostszych postaci to klasa, która reprezentuje daną stronę. Natomiast w tej klasie znajdują się lokatory, które wskazują na elementy (przyciski, pola tekstowe, etc.), z którymi wchodzimy w interakcję podczas testów.
W dużym uproszczeniu jego implementacja polega na następujących krokach:
- Tworzymy nowy moduł (plik), który zawiera klasę, która reprezentuje testowaną stronę.
- Następnie w tej klasie deklarujemy lokatory, które odwołują się do elementów na stronie.
- W testach wykorzystujemy obiekt strony wraz z lokatorami, aby wchodzić w interakcje z elementami strony.
Zawartość pliku login.page.ts:
import { Page } from '@playwright/test' export class LoginPage { constructor(private page: Page) {} loginInput = this.page.getByTestId('login-input') passwordInput = this.page.getByTestId('password-input') loginButton = this.page.getByTestId('login-button') }
Test successful login with correct credentials z pliku login.spec.ts:
test('successful login with correct credentials', async ({ page }) => { // Arrange const userId = loginData.userId; const userPassword = loginData.userPassword; const expectedUserName = 'Jan Demobankowy'; // Act const loginPage = new LoginPage(page) await loginPage.loginInput.fill(userId) await loginPage.passwordInput.fill(userPassword) await loginPage.loginButton.click() // Assert await expect(page.getByTestId('user-name')).toHaveText(expectedUserName); });
Zalety:
- zmniejszenie duplikacji kodu w testach – selektory i lokatory są w jednym miejscu (w klasie strony)
- dobra utrzymywalność – gdy nastąpi zmiana na stronie (np. zmiana ID elementu), to musimy zaktualizować selektory w klasie strony, a testy pozostają bez zmian,
- agregacja danych strony – zebranie w jednym miejscu wszystkich elementów strony, z którymi wchodzimy w interakcję,
- bardzo szybka implementacja,
- dobry dla mniejszych i prostszych testów i projektów.
Wady:
- odpowiedzialność za kolejność działań jest po stronie piszącego testu – osoba pisząca nowe testy musi pamiętać, jakie elementy w jakiej kolejności powinno się używać na danej stronie, np. najpierw wpisanie loginu, później hasła, a na końcu przyciśnięcie przycisku zaloguj,
- słabsza utrzymywalność przy większych projektach lub bardzo dużej liczbie testów – jeśli zmieni się wymagana kolejność działań (np. najpierw hasło, a później login), to będzie wymagana aktualizacja testów.
Hej hej mam 2 pytanka,
Klasę pod loginPage skonstruowałem trochę inaczej
Pytanie pierwsze brzmi czy używając w klasie metod z wykorzystaniem odpowiednich akcji lepiej jest użyć jednej metody która wywołuje kroki po kolei – czy lepiej rozbić to wszystko na osobne metody:
– minusy – większa ilość kodu
– plusy – po każdym kroku można używać asercje w kodzie co pozwoli na dokładniejsze przetestowanie danego działania??
A pytanie drugie to
Użyłem i w testach loginu i w testach desktopa i paymentów takiej konstrukcji w before Each’u
Jak zauważyliście mimo deklaracji w klasie awaita przy metodzie wpisywania id usera musiałem dodać i w beforeEachu awaita dla tej metody ponieważ playwright nie wychwytywał tej metody – wpisywania tekstu w pole na czas i pole loginu zostawało puste – moje pytanie brzmi:
Czy to jest błąd mój czy raczej Playwrighta że nie wychwytuje wpisania z przywołanej z klasy metody asychronicznej – i to tylko na pole loginu bo submit button i wpisanie hasła nie poczebuje już awaita ??
Dzięki z góry za odpowiedź,
Pozdrawiam,
Jakub K
znawczy się w testach loginu nie robiłem before eacha dla logowania tylko do wejścia na stronę i w żadnej z metod nie musiałem potem robić awaita
Więc może to z hookiem Playwright ma problemy wiecie coś o tym ?? dzięki z góry za odpowiedź
Pozdrawiam i spokojności życzę,
Jakub K
Hej,
Sugeruje jednak agregacje akcji (czyli jedną metodę odpowiedzialną za akcje użytkownika, jaką jest logowanie), niż rozbijanie interakcji z elementami na pojedyncze metody.
Więcej o tym opowiadam w kolejnych lekcjach: https://jaktestowac.pl/lesson/pw1s03l08/ 😉 Tam też opowiadam o plusach i minusach tego podejścia 😉
W beforeEach brakuje Ci
await
przed metodami asynchronicznymi:
Bez
await
wywoływanie tych metod może powodować wyścig – pole może nie byc wypełnione przed próbą naciśnięcia przycisku. Bez await akcje i testy będą niestabilne – raz zadziała, a innym razem (i przeważnie) – nie 🙂Hej super – dzięki za wskazówkę i wytłumaczenie.
Pozdrawiam i spokojności życzę,
Jakub K
A ja mam pewną zagwozdkę 🙂 co zrobić z wait-ami i asercjami w stosunku do Page Object?
Załóżmy, że w teście chcę mieć dwie metody ‘login’ i ‘logout’.
Metoda ‘login’ zawiera:
– wprowadzenie loginu,
– hasła,
– kliknięcie ‘Zaloguj’,
– poczekanie (wait) na załadowanie się strony
– sprawdzenie (expect), czy selektor ‘.name’ ma tekst ‘Witaj, ABC’.
Metoda ‘logout’ zawiera:
– kliknięcie przycisku ‘Wyloguj’,
– poczekanie (wait) na wylogowanie
– sprawdzenie (expect), czy ponownie widoczne jest okno logowania.
Czy w Playwright wait-y i asercje nie mogą znajdować się w POM? Zostają w teście?
Czyli wygladało by to tak:
Bardzo dobre pytanie! 🙂
Tak jak piszesz – są dwa podejścia.
Jedno pozwala na umieszczanie asercji zarówno w obiektach stron oraz w testach.
Natomiast drugie – tylko na poziomie testów.
Oba mają swoje plusy i minusy.
Asercje w obiektach stron:
❌Minusem z praktycznego punktu widzenia – jeśli dodamy asercję do metody logowania, to nie będziemy mogli użyć tej metody w teście niepoprawnego logowania.
❌Mniejsza czytelność testów – na poziomie plików
.spec
nie widzimy asercji -> nie wiemy co dokładnie sprawdzamy i co jest kluczowe w danym teście.❌Niezgodne z Single Responsibility Principle
✅Możemy wykorzystywać mechanizm auto-wait – czekanie na wartość elementu jeśli wykorzystamy lokator w expect –
await expect(page.locator('h2')).toHaveText('Logowanie');
✅Implementacja metod jest bardziej zgodna z praktyką Tell Dont Ask
Asercje w testach:
❌Minusem z praktycznego punktu widzenia – jeśli dodamy asercję do metody logowania, to nie będziemy mogli użyć tej metody w teście niepoprawnego logowania.
❌Nie zawsze będzie możliwe czyste i proste wykorzystanie wcześniej wspomnianego mechanizmu auto-wait (chociaż w kolejnych lekcjach pokazuje jak podejść do testów negatywnych).
❌Implementacja metod jest mniej zgodna z praktyką Tell Dont Ask
✅Większa czytelność testów – asercje na poziomie plików
.spec
✅Zgodne z Single Responsibility Principle
Osobiście skłaniałbym się do umieszczania głównych asercji na poziomie testów 🙂 Czyli jesli mam test na logowanie – to asercje umieszczam na poziomie testu na jego końcu. W innych przypadkach, jeśli wykorzystuje logowanie, to mogę w inny sposób zweryfikować czy logowanie się powiodło (np. jesli aplikacja udostępnia API – to za pomocą szybkiej weryfikacji odpowiedzi – za pomocą metody
page.waitForResponse()
:))Jednocześnie nie wykluczałbym czasem potrzeby użycia sprawdzenia czegoś w metodach w obiektach stron. Duzo tu zależy od poziomu skomplikowania testów i samej aplikacji 🙂
PS. Bardziej szczegółowo ten temat będę omawiał w zaawansowanym kursie Przegląd Architektury Testów z pełnego Programu https://jaktestowac.pl/playwright/
Hejka,
mam pytanie związane z przekazywaniem zmiennych.
Czy powinniśmy przypisać wartości z pól obiektu “loginData.username” do zmiennej np. “loginID” czy możemy to sobie odpuścić?
Chodzi mi o to czy wszystkie wartości powinny być wyciągane do zmiennych i wrzucane do sekcji Arrange? Czy jednak można to zostawić w taki sposób jak wkleiłem wyżej?
Skoro zdefiniowałem te pola obiektu loginData, to czy jest sens dodatkowo je przypisywać do kolejnych zmiennych, szczególnie, że format przekazanego argumentu to obiekt.pole i jest on dość jednoznaczny
Hej,
Bardzo dobre pytanie 😉
Oba podejścia mają swoje plusy i minusy:
Wydzielenie do zmiennych:
✅ lepsza widoczność zmiennych i danych które używamy w testach
✅ łatwiejsza zmiana danych, jeśli w teście wykorzystujemy kilka razy dane/obiekt
loginData.username
❌ odrobina duplikacji – dodatkowa zmienna
Używanie danych z loginData bezpośrednio:
✅ mniejsza duplikacja, mniej dodatkowych zmiennych
❌ możemy mieć trudność od razu stwierdzić jakie dane są wykorzystywane w testach – musimy przeglądać cały kod testów
Jeśli mamy lekkie i krótkie testy – powiedziałbym, że można rozważyć podejście nr. 2.
Natomiast gdy mamy bardziej rozbudowane scenariusze – wtedy sugerowałbym podejście 1. 🙂
Dodatkowo pozostaje kwestia spójności kodu w różnych testach – tutaj też sugerowałbym trzymanie sie jednego podejścia. Dzięki temu my się nigdy nie zgubimy w naszym kodzie a wdrażanie nowych osób będzie łatwiejsze 🙂
świetnie, że poruszyliście ten temat i podsumowaliście 🙂
Dzięki! 🙇♂️
hej, mam pytanie o samą koncepcję POM i jej konkretne zastosowanie tutaj.
przejrzałem stronę PW odnośnie POM:
https://playwright.dev/docs/pom
i z tego co widzę jest kilka różnic w stosunku do tego, jak Wy tutaj podchodzicie do zapisu rożnych rzeczy.
Przykładowo, oni pokazują trochę inny styl deklarowania elementów w klasie:
readonly getStartedLink: Locator;
oraz później przypisanie im wartości:
this.getStartedLink = page.locator(‘a’, { hasText: ‘Get started’ });
Wy macie w klasie od razu:
loginInput = this.page.getByTestId(‘login-input’)
Możecie powiedzieć coś więcej na ten temat? Dlaczego nie robicie tak jak twórcy PW? 😀
Czy warto wybrać Wasz sposób zapisu z jakichś konkretnych powodów?
Różnice:
– readonly – dzięki temu mamy pewność, że nie nadpiszemy danej wartości
– deklaracja lokatorów – tutaj nie ma większych różnic (pomijając szczegóły jak to TS/JS później pod spodem ogarniają) – w naszym sposobie sam konstruktor jest wizualnie lżejszy
Najważniejsze, zeby po wyborze danego podejścia konsekwentnie się go trzymać w całym frameworku 🙂
ok dzieki!
Też miałem o to zapytać ponieważ zaciekawiło mnie, że deklarowane zmienne w POM są bez readonly, gdzie u siebie w pracy komercyjnej każdy POM jest tak skonstruowany. Tak jak Krzysiek napisał, najważniejsze to konsekwentne podejście, więc ja zostanę przy readonly 🙂
Część “Przemka” skomentowałem już wcześniej.
Kurs jest świetny więc chciałem się jeszcze odnieść do części Krzyska.
W skrócie – kontynuacja świetnej roboty 🙂
Z POM już pracowałem dlatego w zasadzie starczyły mi materiały udostępnione miesiąc temu czyli do tej lekcji (+ był jeszcze git hub bodajże z 7 lekcji).
Bardzo prosto przekazane są podstawy – mogłem szybko wystartować z POM w typescript. W zasadzie samego Playwrighta potem od razu porzuciłem..
Ale dosyć szybko połapałem się dzięki przerobionemu kursowi w Cypress, który pojawił się z nowym projektem.
Szczerze mówiąc jakbym miał wybierać to osobiście wolałbym Playwright – ale nie zawsze jest dana możliwość wyboru technologii. Zazwyczaj trzeba się dostosować do tego co jest 🙂
Pozdrawiam. Dziękuję za kurs.
Zapewne co jakiś czas będą do Was zaglądać bo sporo fajnych rzeczy tutaj macie.
Dzięki wielkie za pracę i zaangażowanie!
Bardzo się ciesze, że z tego czego się nauczyłeś, to mogłeś wykorzystać w swojej pracy 🙂
Wspominasz też o Cypressie. Osobiście gdybym miał do wyboru narzędzia w nowym proj – to zdecydowanie wybrałbym Playwrighta. Nawet w obecnym projekcie jestem odpowiedzialny za zmianę frameworka – z Cypressa na Playwrighta 🙂
Sam Cypress też jest dobry w konkretnych przypadkach. Z każdego narzędzia jesteśmy w stanie się czegoś nauczyć, a później wykorzystać to doświadczenie w przyszłości 🙂
Do zobaczenia w kolejnych materiałach!