Najprostsza implementacja wzorca POM

TIP: Ta lekcja jest częścią rozwijanego Programu Testy Automatyczne z Playwright 🎭

Prezentacja

Najprostsza implementacja wzorca POM

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 😉

TIP: W tej lekcji używamy kontroli wersji Git. Również w tej sekcji mamy w projekcie skonfigurowane narzędzie Prettier, które automatycznie formatuje 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:

  1. Tworzymy nowy moduł (plik), który zawiera klasę, która reprezentuje testowaną stronę.
  2. Następnie w tej klasie deklarujemy lokatory, które odwołują się do elementów na stronie.
  3. 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.

16 komentarzy

  1. Hej hej mam 2 pytanka,
    Klasę pod loginPage skonstruowałem trochę inaczej

    // Firstly import propernames from playwright test
    import {
      Locator,
      Page,
    } from '@playwright/test';
    
    export class LoginPage {
      // The variables with types
      private page: Page;
      private userId: string;
      private userPasswword: string;
      public loginInnput: Locator;
      public passwordInput: Locator;
      public loginSubmitButton: Locator;
      // For assertion only
      public accountOwner: Locator;
    
      // Constructor
      constructor(page: Page) {
        this.page = page;
        this.loginInnput = this.page.getByTestId('login-input');
        this.passwordInput = this.page.getByTestId('password-input');
        this.loginSubmitButton = this.page.getByTestId('login-button');
        // For assertion only
        this.accountOwner = this.page.getByTestId('user-name');
      }
    
      // Methods
      // User Login type in
      enterUserId = async (userId: string) => {
        await this.loginInnput.fill(userId);
      };
      // User Password type in
      enterUserPassword = async (userPasswword: string) => {
        await this.passwordInput.fill(userPasswword);
      }
      // Login user with credentials 
      submitLoggedUser = async () => {
        // this.enterUserId(this.userId);
        // this.enterUserPassword(this.userPasswword);
        await this.loginSubmitButton.click();
      }
    }
    

    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

      test.beforeEach(async ({ page }) => {
        const userId = loginData.userId;
        const userPassword = loginData.userPassword;
    
        // Declaring our page instance
        const loginPage = new LoginPage(page);
    
        await page.goto('/');
        // new attitude of login operation
        await loginPage.enterUserId(userId);
        loginPage.enterUserPassword(userPassword);
        loginPage.submitLoggedUser();
      });
    

    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

    Avatar Jakub Kruszyński
    1. 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

          // Act LOGIN:
          // Here we need to  creaqte loginPage obhect
          const loginPage = new LoginPage(page);
          // Assertion of proper page display
          await expect.soft(page).toHaveURL(/.*.app/);
          // Act entering the user Login
          loginPage.enterUserId(userId);
          // Soft Assertion - to verify element which is less important:
          await expect.soft(loginPage.loginInnput).toHaveValue(userId);
          // Act enterting user password
          loginPage.enterUserPassword(userPassword);
          // Soft Assertion - to verify element which is less important:
          await expect.soft(loginPage.passwordInput).toHaveValue(userPassword);
          // Submit login button
          // await page.getByTestId('login-button').click();
          loginPage.submitLoggedUser();
      

      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

      Avatar Jakub Kruszyński
    2. 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:

          loginPage.enterUserPassword(userPassword);
          loginPage.submitLoggedUser();
      

      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 🙂

      Krzysiek Kijas Krzysiek Kijas
  2. 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:

    await login.fillInLoginAndPasswordAdmin();
    await page.waitForTimeout(3000);
    await expect(page.locator('.name')).toHaveText('Witaj, ABC');
    await logout.logoutMP();
    await page.waitForTimeout(3000);
    await expect(page.locator('h2')).toHaveText('Logowanie');
    
    Avatar Monika
    1. 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/

      Krzysiek Kijas Krzysiek Kijas
  3. Hejka,

    mam pytanie związane z przekazywaniem zmiennych.

    test('successful login with correct username', async ({ page }) => {
        const expectedUsername = 'Jan Demobankowy';
    
        await page.goto('/');
    
        const loginPage = new LoginPage(page)
        await loginPage.loginInput.fill(loginData.username);
    

    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

    Avatar Tomasz Guzik
    1. 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 🙂

      Krzysiek Kijas Krzysiek Kijas
  4. 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?

    Avatar Michał Dudziak
    1. 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 🙂

      Krzysiek Kijas Krzysiek Kijas
      1. 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 🙂

        Avatar Dominik Calak
  5. 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.

    Avatar Łukasz
    1. 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!

      Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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