Mire jó az AI? Hát például arra, hogy ha írni szeretnék egy témáról, de lusta vagyok, az egyébként is Google segítsével összeszedett információkat rendszerbe szedni, akkor relatív gyorsan kézhez kapok egy bejegyzést.
A Page Object Model (POM) a szoftvertesztelés-automatizálás egyik legnépszerűbb és leghatékonyabb tervezési mintája (Design Pattern). A lényege, hogy az alkalmazás egyes weboldalait (vagy oldalrészeit) különálló objektumokként képezi le a kód szintjén.
Miért érdemes használni?
- Karbantarthatóság: Ha megváltozik egy gomb ID-ja vagy egy mező elhelyezkedése, elegendő azt egyetlen helyen (a Page Object osztályban) átírni, a teszteseteket nem kell bántani.
- Kód újrafelhasználhatósága: A gyakori műveletek (pl. bejelentkezés, keresés) egyszer vannak megírva, és bármelyik tesztből meghívhatók.
- Olvashatóság: A tesztkód tiszta, átlátható és üzleti logikát követő lesz, rejtve a techniai megvalósítást (pl. DOM-lekérdezéseket).
Minta megvalósítás (Python + Selenium)
Az alábbiakban egy klasszikus bejelentkezési folyamat példáján keresztül látható a POM működése.
A Page Object osztály (login_page.py)
Ez az osztály tartalmazza az oldal elemeinek azonosítóit (lokátorait) és a végezhető műveleteket.
Python
from selenium.webdriver.common.by import By
class LoginPage:
def __init__(self, driver):
self.driver = driver
# Lokátorok (különválasztva a logikától)
self._username_input = (By.ID, "username")
self._password_input = (By.ID, "password")
self._login_button = (By.CSS_SELECTOR, "button[type='submit']")
self._error_message = (By.CLASS_NAME, "flash-error")
# Akciók / Műveletek
def enter_username(self, username: str):
self.driver.find_element(*self._username_input).send_keys(username)
def enter_password(self, password: str):
self.driver.find_element(*self._password_input).send_keys(password)
def click_login(self):
self.driver.find_element(*self._login_button).click()
# Összetett folyamat (Láncolt / Magasabb szintű akció)
def login(self, username: str, password: str):
self.enter_username(username)
self.enter_password(password)
self.click_login()
def get_error_message(self) -> str:
return self.driver.find_element(*self._error_message).text
A Teszteset (test_login.py)
A tesztkód csak a Page Object által nyújtott metódusokat használja, így végtelenül egyszerű és olvasható.
Python
import pytest
from selenium import webdriver
from login_page import LoginPage
@pytest.fixture
def driver():
driver = webdriver.Chrome()
driver.get("https://example.com/login")
yield driver
driver.quit()
def test_successful_login(driver):
login_page = LoginPage(driver)
# Bejelentkezés végrehajtása
login_page.login("valid_user", "correct_password")
# Ellenőrzés (Assertion)
assert driver.current_url == "https://example.com/dashboard"
def test_failed_login(driver):
login_page = LoginPage(driver)
login_page.login("invalid_user", "wrong_password")
# Hibaüzenet ellenőrzése
assert "Érvénytelen adatok" in login_page.get_error_message()
Jó tanácsok a gyakorlati alkalmazáshoz
- Ne tegyél assertion-öket a Page Object-be: A
assertkifejezések a tesztfájlban maradjanak, a Page Object csak adatot adjon vissza vagy állapotot módosítson. - Page Factory használata: Bizonyos keretrendszerekben (pl. Java + Selenium) a
@FindByannotációkkal még tisztább szerkezet érhető el. - Lazy loading: Az elemeket csak akkor kérd le a DOM-ból, amikor ténylegesen szükség van rájuk (például dinamikus felületek esetén).
A Page Object Model (POM) megvalósítása a modernebb eszközökkel (mint a Playwright vagy a Selenium Java-alapú változata) még letisztultabbá válik, mivel ezek a keretrendszerek beépített architektúrával támogatják a megbízhatóbb elemkezelést.
Az alábbiakban mindkét megközelítésre találsz egy-egy komplett kódmintát.
Playwright + TypeScript (Ajánlott / Modern megközelítés)
A Playwright eleve magában foglalja az automatikus várakozást (Auto-waiting), így nincs szükség explicit wait-ek kézi kezelésére.
A Page Object osztály (pages/LoginPage.ts)
TypeScript
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
self.page = page;
// A Playwright-ban a lokátorok deklaratívak, nem kérdezik le azonnal a DOM-ot
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.flash-error');
}
async goto() {
await this.page.goto('https://example.com/login');
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() || '';
}
}
A Tesztfájl (tests/login.spec.ts)
TypeScript
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Bejelentkezés tesztek', () => {
let loginPage: LoginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.goto();
});
test('Sikeres bejelentkezés', async ({ page }) => {
await loginPage.login('valid_user', 'correct_password');
// Ellenőrzés Playwright assert-tel
await expect(page).toHaveURL('https://example.com/dashboard');
});
test('Hibás bejelentkezés', async () => {
await loginPage.login('invalid_user', 'wrong_password');
await expect(loginPage.errorMessage).toBeVisible();
await expect(loginPage.errorMessage).toContainText('Érvénytelen adatok');
});
});
Java + Selenium (Klasszikus keretrendszer)
Java környezetben a PageFactory és a @FindBy annotációk használata a legelterjedtebb POM minta.
A Page Object osztály (LoginPage.java)
Java
package pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class LoginPage {
private WebDriver driver;
// Lokátorok deklarálása annotációkkal
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(className = "flash-error")
private WebElement errorMessage;
// Konstruktor: inicializálja a PageFactory elemeit
public LoginPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void enterUsername(String username) {
usernameInput.clear();
usernameInput.sendKeys(username);
}
public void enterPassword(String password) {
passwordInput.clear();
passwordInput.sendKeys(password);
}
public void clickLogin() {
loginButton.click();
}
public void login(String username, String password) {
enterUsername(username);
enterPassword(password);
clickLogin();
}
public String getErrorMessageText() {
return errorMessage.getText();
}
}
A Tesztosztály (JUnit 5 használatával) (LoginTest.java)
Java
package tests;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import pages.LoginPage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class LoginTest {
private WebDriver driver;
private LoginPage loginPage;
@BeforeEach
public void setUp() {
driver = new ChromeDriver();
driver.manage().window().maximize();
driver.get("https://example.com/login");
loginPage = new LoginPage(driver);
}
@Test
public void testSuccessfulLogin() {
loginPage.login("valid_user", "correct_password");
assertEquals("https://example.com/dashboard", driver.getCurrentUrl());
}
@Test
public void testFailedLogin() {
loginPage.login("invalid_user", "wrong_password");
assertTrue(loginPage.getErrorMessageText().contains("Érvénytelen adatok"));
}
@AfterEach
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
Főbb különbségek a két megközelítés között
| Szempont | Playwright (TypeScript) | Selenium (Java) |
| Lokátorok | A Locator objektumok deklaratívak (auto-wait) | A @FindBy elemek lekezeléséhez várakozás (WebDriverWait) kellhet |
| Párhuzamosítás | Alapból támogatott a tesztfutztatóban | TestNG vagy JUnit szálkezelést igényel |
| Inicializálás | Egyszerű objektumkonstruktor | PageFactory.initElements() hívás szükséges |
A Playwright Custom Fixture funkciója segítségével a Page Object osztályok példányosítását teljesen kiszervezheted a tesztekből. Ezzel elkerülhető, hogy minden egyes tesztfájlban manuálisan kelljen megadni a const loginPage = new LoginPage(page) sorokat.
A tesztek így közvetlenül paraméterként kérhetik el a szükséges oldalobjektumokat, az infrastruktúra pedig automatikusan példányosítja és előkészíti azokat.
A Page Object osztályok (pages/)
A megszokott módon hozzuk létre az oldalklasszokat.
TypeScript
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.locator('#username');
}
async goto() {
await this.page.goto('/login');
}
}
// pages/DashboardPage.ts
import { Page, Locator } from '@playwright/test';
export class DashboardPage {
readonly page: Page;
readonly userMenu: Locator;
constructor(page: Page) {
this.page = page;
this.userMenu = page.locator('#user-menu');
}
}
A Custom Fixture létrehozása (fixtures/baseTest.ts)
Itt terjesztjük ki a Playwright alapértelmezett test objektumát, megadva a saját Page Object fixture-jeinket.
TypeScript
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
// 1. Típusdefiníció a fixture-ökhöz
type MyFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
};
// 2. A 'base.extend' használatával regisztráljuk a saját fixture-jeinket
export const test = base.extend<MyFixtures>({
// Automatikusan példányosítja a LoginPage-t és igény esetén megnyitja az oldalt
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto(); // Akár előkészítő lépéseket is tehetünk ide
await use(loginPage); // Átadja az objektumot a tesztnek
},
// Példányosítja a DashboardPage-t
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
});
// Az 'expect'-et is exportáljuk a kényelmes importálásért
export { expect } from '@playwright/test';
A Fixture-ök használata a tesztekben (tests/login.spec.ts)
A @playwright/test helyett a saját fixtures/baseTest fájlunkból importáljuk a test objektumot. A tesztfüggvény paramétereként megadott nevek alapján a Playwright automatikusan beadagolja a megfelelő példányokat.
TypeScript
// Figyelem: A saját kiterjesztett test objektumunkat importáljuk!
import { test, expect } from '../fixtures/baseTest';
test('Sikeres bejelentkezés custom fixture-rel', async ({ loginPage, dashboardPage }) => {
// A loginPage.goto() már lefutott a fixture-ben!
await loginPage.usernameInput.fill('standard_user');
// A dashboardPage is azonnal használható, nem kellett 'new'-val példányosítani
await expect(dashboardPage.userMenu).toBeVisible();
});
Miért ez a legnépszerűbb Playwright architektúra?
- Nulla boilerplate kód: Nem kell
beforeEachblokkokban példányosítani az osztályokat. - Lazy loading (Lusta betöltés): Csak azok a Page Object-ek példányosulnak, amiket a teszt fejlécében (
{ loginPage, dashboardPage }) kifejezetten elkérsz. - Beépített tear-down: A fixture-ben a
use()hívás utáni kódrészlet automatikusan lefut a teszt végén (takarításra, kijelentkezésre ideális).