Page Object Model i agregacja akcji

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

Prezentacja

Page Object Model i agregacja akcji

Page Object Model i agregacja akcji

Page Object Model i agregacja akcji

Page Object Model i agregacja akcji

Dodatkowe materiały

Bazujemy na kodzie lekcji L07_pom_component

Kod wynikowy tej lekcji znajduje się tu: L08_pom_aggregate

Pamiętaj, aby po danej porcji pracy: uruchamiać test, commitować poprawnie działający kod 😉

Przeniesienie zmiennej do sekcji describe()

Page Object Model i agregacja akcji

Page Object Model i agregacja akcji

Zawartość pliku login.spec.ts, gdzie wykorzystujemy zmienną globalną loginPage. Jej deklaracja znajduje się w cele test.describe, inicjalizacja w beforeEach, a użycie – w testach:

import { test, expect } from '@playwright/test';
import { loginData } from '../test-data/login.data';
import { LoginPage } from '../pages/login.page';
import { PulpitPage } from '../pages/pulpit.page';


test.describe('User login to Demobank', () => {
  let loginPage: LoginPage;


  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    loginPage = new LoginPage(page);
  });


  test('successful login with correct credentials', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const userPassword = loginData.userPassword;
    const expectedUserName = 'Jan Demobankowy';


    // Act
    await loginPage.loginInput.fill(userId);
    await loginPage.passwordInput.fill(userPassword);
    await loginPage.loginButton.click();


    // Assert
    const pulpitPage = new PulpitPage(page);
    await expect(pulpitPage.userNameText).toHaveText(expectedUserName);
  });


  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    await loginPage.loginInput.fill(incorrectUserId);
    await loginPage.passwordInput.click();


    // Assert
    await expect(loginPage.loginError).toHaveText(expectedErrorMessage);
  });


  test('unsuccessful login with too short password', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const incorrectPassword = '1234';
    const expectedErrorMessage = 'hasło ma min. 8 znaków';


    // Act
    await loginPage.loginInput.fill(userId);
    await loginPage.passwordInput.fill(incorrectPassword);
    await loginPage.passwordInput.blur();


    // Assert
    await expect(loginPage.passwordError).toHaveText(expectedErrorMessage);
  });
});

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 w prosty sposób rozszerzymy implementację z poprzednich lekcji 🙂

Page Object Model z agregacją akcji

Page Object Model z agregacją akcji 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,
  • metody, które są zebranymi akcjami na poszczególnych elementach.

Dzięki zebraniu akcji w metodach, w testach nie musimy się odnosić do pojedynczych elementów strony.

Zawartość pliku login.spec.ts, gdzie w teście successful login with correct credentials wykorzystujemy nową implementację wzorca POM:

import { test, expect } from '@playwright/test';
import { loginData } from '../test-data/login.data';
import { LoginPage } from '../pages/login.page';
import { PulpitPage } from '../pages/pulpit.page';


test.describe('User login to Demobank', () => {
  let loginPage: LoginPage;


  test.beforeEach(async ({ page }) => {
    await page.goto('/');
    loginPage = new LoginPage(page);
  });


  test('successful login with correct credentials', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const userPassword = loginData.userPassword;
    const expectedUserName = 'Jan Demobankowy';


    // Act
    await loginPage.login(userId, userPassword);


    // Assert
    const pulpitPage = new PulpitPage(page);
    await expect(pulpitPage.userNameText).toHaveText(expectedUserName);
  });


  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    await loginPage.loginInput.fill(incorrectUserId);
    await loginPage.passwordInput.click();


    // Assert
    await expect(loginPage.loginError).toHaveText(
      expectedErrorMessage
    );
  });


  test('unsuccessful login with too short password', async ({ page }) => {
    // Arrange
    const userId = loginData.userId;
    const incorrectPassword = '1234';
    const expectedErrorMessage = 'hasło ma min. 8 znaków';


    // Act
    await loginPage.loginInput.fill(userId);
    await loginPage.passwordInput.fill(incorrectPassword);
    await loginPage.passwordInput.blur();


    // Assert
    await expect(loginPage.passwordError).toHaveText(
      expectedErrorMessage
    );
  });
});

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');


  loginError = this.page.getByTestId('error-login-id');
  passwordError = this.page.getByTestId('error-login-password');


  async login(userId: string, userPassword: string): Promise<void> {
    await this.loginInput.fill(userId);
    await this.passwordInput.fill(userPassword);
    await this.loginButton.click();
  }
}

Zalety:

  • zmniejszenie duplikacji kodu w testach – selektory, lokatory oraz akcje na lokatorach są w jednym miejscu (w klasie strony),
  • jeszcze lepsza utrzymywalność – gdy nastąpi zmiana na stronie (zarówno w elemencie, jak i w kolejności akcji), to musimy zaktualizować klasę strony, a testy pozostają bez zmian,
  • agregacja danych strony – zebranie w jednym miejscu wszystkich elementów strony, z którymi wchodzimy w interakcję,
  • odpowiedzialność za kolejność działań jest w kodzie – osoba pisząca nowe testy nie 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,
  • dość szybka implementacja,
  • sprawdzi się zarówno w mniejszych, prostszych testach/projektach, oraz również w tych średniej wielkości.

Wady:

  • wymagana lepsza znajomość programowania – wprowadzenie metod agregujących akcje może podnieść poziom skomplikowania kodu,
  • pewne ograniczenia – stworzona metoda powinna mieć jedną odpowiedzialność (trzymając się założeń programistycznych np: SOLID) tzn. powinna być odpowiedzialną za jedną konkretną kompleksową akcję. Gdy mamy poprawne logowanie i niepoprawne logowanie – to nie możemy wykorzystać tej samej metody, bo atomowe akcje nie będą się zgadzały – o tym dokładniej poczytasz dalej w tej lekcji 🙂

Page Object Model i inne wariacje

TIP: W tym materiale pokazujemy przykłady prostych implementacji wzorca POM. W przyszłych zaawansowanych kursach zaprezentujemy również bardziej złożone implementacje, które w dużych projektach zapewnią jeszcze lepszą utrzymywalność i rozszerzalność kodu. W zaawansowanych materiałach pokażemy zastosowanie dodatkowych wzorców i praktyk, jak dziedziczenie czy kompozycja. W obecnych lekcjach skupiamy się na budowaniu niezbędnych podstaw.

Ważne aby zapamiętać: Nie ma idealnego rozwiązania, które sprawdzi się w każdym projekcie, a każdy z rozważanych sposobów implementacji ma swoje wady i zalety.

Jak podejść do wyboru sposobu implementacji testów w POM:

  • przeanalizuj każdy ze sposobów pod kątem tego, czego potrzebujesz w projekcie,
  • przedyskutuj podejścia z zespołem i wypracujcie wspólne zdanie i rozwiązanie,
  • zapisz wybrane rozwiązanie w dokumentacji projektu (np. w decision logu albo w coding standards),
  • podczas implementacji zachowuj spójność z wybranym podejściem 😉
  • pamiętaj, że każde rozwiązanie można w przyszłości zmienić (co jednak może wiązać się z kosztami).

Testy negatywne a POM

Czasem w testach front-endu chcemy przetestować niepoprawne ścieżki użytkownika np.:

  • niepoprawne logowanie,
  • niepoprawny zakup gdy nie dodano żadnych produktów albo konto użytkownika jest niepoprawnie skonfigurowane,
  • próba doładowania nieistniejącej karty, gdy nie mamy dostatecznych środków na koncie,
  • notowanie wyników sportowych z błędnymi danymi.

Przy takich testach musimy sobie najpierw odpowiedzieć na pytania:

  • czy jest sens je implementować po stronie testów UI/e2e?
  • czy te przypadki można pokryć na niższych poziomach? np. jako testy jednostkowe front-endu, które są pisane przez developerów?

W pewnych przypadkach, po analizie wyjdzie nam, że musimy pokryć takie przypadki, bo są one krytyczne dla naszego systemu. Również może się okazać, że możemy je pokryć jedynie za pomocą testów front-endu, bo ważna jest walidacja w warstwie front-endu (a pośrednio testujemy komunikację z back-endem).

W sytuacji gdy posiadamy przypadki testowe, wykonaliśmy analizę “Czy warto te testy zautomatyzować?” i ustaliliśmy, że tak – to możemy zastanowić się jak ugryźć ten temat.

Tutaj może pojawić się wyzwanie: Jak to poprawnie zrobić, w przypadku gdy w testach mamy POM, który koncentruje się na poprawnych ścieżkach użytkownik?

Jest na to kilka sposobów.

Sposób 1 – bazowanie na lokatorach (nasz dotychczasowy sposób)

Test na niepoprawne logowanie z pliku login.spec.ts:

  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    const loginPage = new LoginPage(page)
    await loginPage.loginInput.fill(incorrectUserId)
    await loginPage.passwordInput.click();


    // Assert
    await expect(loginPage.loginError).toHaveText(
      expectedErrorMessage
    );
  });

Zalety:

  • duża dowolność w komponowaniu testów – możemy używać różnych akcji w różnej kolejności w zależności od potrzeby.

Wady:

  • gorsza utrzymywalność kodu testówkorzystamy z lokatorów, zamiast z metod agregujących akcje użytkownika. Gdy np. zmieni się wymagana kolejność działań będziemy musieli naprawić wszystkie testy, w których korzystamy z tych akcji. Będzie to szczególnie uciążliwe przy dużej liczbie testów.
  • [w zależności od ustaleń projektowych] łamiemy założenia POM – jeśli założyliśmy w projekcie, że zawsze agregujemy akcje użytkownika w metodach (i tylko ich używamy w testach), to w tym rozwiązaniu tworzymy wyjątek i łamiemy własne ustalenia.

Sposób 2 – dedykowane metody do niepoprawnych akcji użytkownika

W tym sposobie implementujemy nową metodę, w której wykonamy akcje wymagane w niepoprawnej ścieżce użytkownika. W naszym przypadku, w teście niepoprawnego logowania, zaimplementujemy metodę do wpisywania danych przy logowaniu, ale bez przyciskania przycisku zaloguj.

Test na niepoprawne logowanie z pliku login.spec.ts:

  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    const loginPage = new LoginPage(page)
    await loginPage.fillLoginCredentials(incorrectUserId, '')


    // Assert
    await expect(loginPage.loginError).toHaveText(
      expectedErrorMessage
    );
  });

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


    async login(userId: string, userPassword: string): Promise<void> {
        await this.loginInput.fill(userId);
        await this.passwordInput.fill(userPassword);
        await this.loginButton.click();
    }


    async fillLoginCredentials(userId: string, userPassword: string): Promise<void> {
        await this.loginInput.fill(userId);
        await this.passwordInput.fill(userPassword);
        await this.passwordInput.blur();
    }
}

Zalety:

  • dedykowane metody – możemy ich użyć podczas testowania niepoprawnych ścieżek użytkownika,
  • agregacja akcji – pojedyncze akcje są zagregowane w metodzie, co zmniejsza duplikację kodu w testach.

Wady:

  • duplikacja kodu w implementacji obiektów stron – możemy mieć kilka metod, które agregują kilkanascie akcji użytkownika, ale różnią się jedną akcją. W naszym przypadku mamy dwie podobne metody, które nieznacznie się różnią między sobą (w jednej jest click(), a w drugiej blur()).

Sposób 3 – wywoływanie metod z metod

W tym sposobie wywołujemy mniejsze metody (wpisane danych logowania) w większych metodach (logowanie do aplikacji).

Test na niepoprawne logowanie z pliku login.spec.ts:

  test('unsuccessful login with too short username', async ({ page }) => {
    // Arrange
    const incorrectUserId = 'tester';
    const expectedErrorMessage = 'identyfikator ma min. 8 znaków';


    // Act
    await loginPage.fillLoginCredentials(incorrectUserId, '')


    // Assert
    await expect(loginPage.loginError).toHaveText(
      expectedErrorMessage
    );
  });

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


    async login(userId: string, userPassword: string): Promise<void> {
        await this.fillLoginCredentials(userId, userPassword)
        await this.loginButton.click();
    }


    async fillLoginCredentials(userId: string, userPassword: string): Promise<void> {
        await this.loginInput.fill(userId);
        await this.passwordInput.fill(userPassword);
        await this.passwordInput.blur();
    }
}

Zalety:

  • zmniejszenie duplikacji kodu i używanie istniejących metod,
  • łatwiejsze i mniej kosztowne utrzymanie – łatwiej wprowadzać zmiany lub poprawki do kodu i akcji użytkownika, które mamy zagregowane w jednym miejscu.

Wady:

  • czasem możemy potrzebować wykonać dodatkową akcję, która nie jest wymagana w poprawnej ścieżce (np. nadmiarowa akcja blur() podczas wykonywania akcji logowania).

Podsumowanie

Podobnie jak z wyborem wariantu POM, tak tutaj też nie ma idealnego rozwiązania, które sprawdzi się w każdym projekcie, a każdy z powyższych sposobów implementacji ma swoje wady i zalety. Przed wyborem danego sposobu zastanów się i rozważ plusy i minusy 🙂

W przypadku tej lekcji finalnie zdecydowaliśmy się na pozostawienie lokatorów w negatywnych testach. Da nam to maksymalną swobodę przy projektowaniu nowych przypadków kosztem czasochłonności refaktoryzacji w przypadku zmian, np kolejności danych akcji lub potrzeby dodatkowych kroków.

4 komentarze

  1. Mam pytanie o Promise. Czy w każdej funkcji, która nic nie zwraca, powinniśmy wpisywać Promise, czy też można to pomijać (bo kod i tak się wykona)? Jakie jest uzasadnienie dla wpisywania tego?

    Avatar Sławomir Duda-Klimaszewski
      1. Hej,
        Osobiście sugerowałbym zawsze dodawać 🙂
        Z pozoru wydaje się nadmiarowe, ale ma sporo plusów.
        Przy dodawaniu również tego typu zwracanego mamy pełna spójność i czytelność z pozostałymi metodami, gdzie dodaliśmy typ zwracany. Dodatkowo – zabezpieczy nas to przed sytuacjami, gdy modyfikujemy daną metodę/funkcję i chcemy aby zwracała jakąś wartość.
        Wtedy świadomie będziemy musieli też zmienić typ zwracany i unikniemy potencjalnych błędów 🙂

        Krzysiek Kijas Krzysiek Kijas

Dodaj komentarz

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