W automatyzacji testów struktura i organizacja kodu mają ogromne znaczenie dla przejrzystości, skalowalności oraz łatwości utrzymania. Jednym z najczęściej używanych wzorców projektowych w testach automatycznych jest Page Object Model (POM). W tym artykule omówimy, jakie są jego zalety i podpowiemy, jak można go zaimplementować w narzędziu Playwright na przykładzie testów aplikacji webowej.
Czym jest Page Object Model?
Page Object Model to wzorzec projektowy używany w automatyzacji testów, który polega na tworzeniu obiektów reprezentujących poszczególne strony aplikacji. Każdy taki obiekt zawiera metody, które odpowiadają interakcjom użytkownika z elementami danej strony. Dzięki temu logika testów jest oddzielona od logiki dotyczącej interakcji z elementami UI, co upraszcza zarządzanie i utrzymanie kodu testowego. Jest to szczególnie przydatne w przypadku dużych projektów, gdzie zmiany w interfejsie użytkownika mogą być częste i złożone.
Page Object Model – jakie ma zalety?
- Modularność Jak wskazaliśmy, struktura POM pozwala na oddzielenie logiki testowej od logiki dotyczącej interakcji z elementami UI. Dzięki temu zmiany w interfejsie użytkownika wymagają modyfikacji tylko w jednym miejscu – w odpowiednim obiekcie strony. To znacząco redukuje czas i wysiłek potrzebny na utrzymanie testów.
- Czytelność: Testy stają się bardziej przejrzyste i zrozumiałe, ponieważ metody odpowiadające interakcjom są dobrze nazwane i zorganizowane. To ułatwia zrozumienie logiki testów nawet przez osoby, które nie są bezpośrednio zaangażowane w ich tworzenie.
- Reużywalność: Metody zdefiniowane w obiektach stron mogą być wielokrotnie używane w różnych testach, co zmniejsza ilość duplikowanego kodu. Dzięki temu testy są bardziej efektywne i łatwiejsze do rozszerzania.
- Łatwość utrzymania: Gdy interfejs użytkownika ulega zmianie, wystarczy zaktualizować odpowiednie obiekty stron, co minimalizuje czas przeznaczony na aktualizację testów. To sprawia, że POM jest idealnym rozwiązaniem dla zespołów pracujących nad dużymi, dynamicznie zmieniającymi się aplikacjami.
Implementacja Page Object Model w Playwright
Na przykładzie poniżej omówimy, jak zaimplementować POM w Playwright, tworząc obiekt reprezentujący stronę logowania.
Załóżmy, że mamy prostą aplikację z formularzem logowania.
Struktura projektu
Na potrzeby naszego przykładu, struktura projektu będzie wyglądać następująco:
project
|-- tests
| |-- login.spec.js
|-- pages
| |-- login.page.js
|-- playwright.config.js
Implementacja obiektu strony
Najpierw zdefiniujemy obiekt strony LoginPage w pliku login.page.js. Obiekt ten będzie zawierał wszystkie elementy i metody potrzebne do interakcji ze stroną logowania.
const { expect } = require('@playwright/test');
class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
module.exports = { LoginPage };
W powyższym kodzie LoginPage jest klasą reprezentującą stronę logowania. Zawiera ona metody goto() do nawigacji do strony logowania oraz login(), która wypełnia pola formularza i klika przycisk „Submit”.
Implementacja testu z użyciem POM
Następnie, zaimplementujemy test korzystający z LoginPage w pliku login.spec.js. Test ten będzie sprawdzał poprawność funkcji logowania.
const { test, expect } = require('@playwright/test');
const { LoginPage } = require('../pages/login.page');
test.describe('Login tests', () => {
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('.welcome-message')).toBeVisible();
await expect(page.locator('input[name="search"]')).toBeEnabled();
});
test('failed login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'wrongpassword');
await expect(page.locator('.error-message')).toHaveText('Invalid credentials');
});
});
W powyższym kodzie używamy obiektu LoginPage do zarządzania interakcjami ze stroną logowania. Pierwszy test sprawdza poprawne logowanie, natomiast drugi sytuację, gdy logowanie kończy się niepowodzeniem.
Rozbudowanie Page Object Model
POM można rozwijać i dostosowywać do bardziej złożonych scenariuszy testowych. Obejmuje to dziedziczenie, kompozycję oraz dodawanie dodatkowych funkcji pomocniczych, które mogą być używane na różnych stronach aplikacji.
Dziedziczenie
Dziedziczenie pozwala na tworzenie hierarchii klas, w której wspólne metody i właściwości mogą być zdefiniowane w klasach bazowych i dziedziczone przez klasy podrzędne. Tworzymy bazową klasę BasePage, która zawiera metody i elementy wspólne dla wszystkich stron.
class BasePage {
constructor(page) {
this.page = page;
}
async goto(url) {
await this.page.goto(url);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
}
Klasy reprezentujące konkretne strony mogą dziedziczyć z BasePage, co pozwala na ponowne użycie wspólnych metod i elementów.
const { BasePage } = require('./base.page');
class LoginPage extends BasePage {
constructor(page) {
super(page);
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
module.exports = { LoginPage };
Dzięki dziedziczeniu możemy uniknąć duplikacji kodu i skupić się na specyficznych funkcjonalnościach dla każdej strony, jednocześnie korzystając z wspólnych metod i właściwości zdefiniowanych w klasie bazowej.
Kompozycja
Kompozycja pozwala na tworzenie bardziej modularnych i elastycznych obiektów, które mogą zawierać inne obiekty. Na przykład, możemy stworzyć komponent HeaderComponent, który będzie używany na różnych stronach, zamiast duplikować kod w każdej klasie strony.
class HeaderComponent {
constructor(page) {
this.page = page;
this.profileButton = page.locator('.profile-button');
}
async gotoProfile() {
await this.profileButton.click();
}
}
class DashboardPage extends BasePage {
constructor(page) {
super(page);
this.header = new HeaderComponent(page);
this.searchInput = page.locator('input[name="search"]');
}
async search(query) {
await this.searchInput.fill(query);
await this.page.locator('button[type="submit"]').click();
}
}
module.exports = { DashboardPage };
W powyższym przykładzie DashboardPage używa HeaderComponent, co pozwala na ponowne użycie logiki nagłówka na różnych stronach aplikacji. Kompozycja umożliwia bardziej elastyczne zarządzanie kodem i ułatwia jego utrzymanie.
Dodatkowe techniki i dobre praktyki
Aby w pełni wykorzystać potencjał Page Object Model, warto zwrócić uwagę na dodatkowe techniki i dobre praktyki, które mogą jeszcze bardziej usprawnić proces tworzenia i utrzymania testów automatycznych.
Używanie wzorców projektowych
Page Object Model to jeden z wielu wzorców projektowych, które mogą być stosowane w testach automatycznych. Inne wzorce, takie jak Factory Pattern czy Singleton Pattern, mogą być używane w połączeniu z POM, aby jeszcze bardziej zwiększyć modularność i skalowalność testów.
Parametryzacja testów
Parametryzacja testów pozwala na uruchamianie tego samego testu z różnymi danymi wejściowymi. W Playwright można to osiągnąć za pomocą metody test.each. Używając tej techniki, możemy zmniejszyć ilość kodu testowego i zwiększyć pokrycie testowe.
test.describe('Login tests', () => {
test.each([
{ username: 'validUser', password: 'validPass', expected: 'Dashboard' },
{ username: 'invalidUser', password: 'invalidPass', expected: 'Invalid credentials' }
])('login test', async ({ page, username, password, expected }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(username, password);
if (expected === 'Dashboard') {
await expect(page.locator('h1')).toHaveText(expected);
} else {
await expect(page.locator('.error-message')).toHaveText(expected);
}
});
});
Asynchroniczność i oczekiwanie na elementy
W testach automatycznych bardzo ważne jest odpowiednie zarządzanie asynchronicznością. W Playwright można korzystać z metod takich jak waitForSelector, aby upewnić się, że elementy są dostępne, zanim spróbujemy z nimi interagować.
class LoginPage extends BasePage {
constructor(page) {
super(page);
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
}
async login(username, password) {
await this.page.waitForSelector(this.usernameInput);
await this.usernameInput.fill(username);
await this.page.waitForSelector(this.passwordInput);
await this.passwordInput.fill(password);
await this.page.waitForSelector(this.submitButton);
await this.submitButton.click();
}
}
Testy w różnych przeglądarkach i na różnych urządzeniach
Jednym z kluczowych elementów testowania aplikacji webowych jest upewnienie się, że działa ona poprawnie w różnych przeglądarkach i na różnych urządzeniach. Playwright umożliwia uruchamianie testów w wielu przeglądarkach jednocześnie, co jest niezwykle przydatne w kontekście zapewnienia jakości.
test.describe('Cross-browser tests', () => {
test('login in Chromium, Firefox, and WebKit', async ({ page, browserName }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('testuser', 'password123');
await expect(page.locator('h1')).toHaveText('Dashboard');
await expect(page.locator('.welcome-message')).toBeVisible();
await expect(page.locator('input[name="search"]')).toBeEnabled();
});
});
Podsumowanie
Page Object Model jest wzorcem projektowym, który dzięki oddzieleniu logiki testowej od logiki interakcji z elementami UI sprawia, że testy stają się bardziej przejrzyste, łatwe do utrzymania i zarządzania oraz skalowalne.
Wprowadzenie POM pozwala na łatwiejsze zarządzanie testami oraz szybsze reagowanie na zmiany w interfejsie użytkownika. Zalety takie jak modularność, czytelność, reużywalność i łatwość utrzymania sprawiają, że jest to idealne rozwiązanie dla zespołów pracujących nad dynamicznie rozwijającymi się aplikacjami.
Dzięki zaawansowanym technikom, jak dziedziczenie, kompozycja, parametryzacja testów, odpowiednie zarządzanie asynchronicznością czy testowanie w różnych przeglądarkach i na różnych urządzeniach, można jeszcze bardziej zwiększyć efektywność i skuteczność testów automatycznych. Wykorzystanie Playwright w połączeniu z POM pozwala na tworzenie nowoczesnych, elastycznych i skalowalnych rozwiązań testowych, które sprostają wymaganiom nawet najbardziej złożonych aplikacji webowych.