![]()
W artykule Page Object Pattern – czyli jak przestać kopiować XPath-y po projekcie poznasz Page Object Pattern – jeden z najczęściej stosowanych wzorców projektowych w testach automatycznych aplikacji webowych.
Dowiesz się, na czym polega jego idea, jak wygląda jego struktura, oraz dlaczego pozwala pisać czytelniejsze i łatwiejsze w utrzymaniu testy.
Na koniec zobaczysz praktyczny przykład zastosowania POM-a, dzięki któremu unikniesz powielania XPath-ów i chaosu w kodzie testowym.
Czym jest POM i po co go używać
Page Object Pattern (POM) to prosty wzorzec projektowy wykorzystywany w testach automatycznych.
Dzięki niemu można oddzielić logikę testów od warstwy technicznej, co ułatwia utrzymanie, poprawia czytelność i zmniejsza liczbę błędów.
Page Object Pattern to prosty wzorzec projektowy wykorzystywany w testach automatycznych, który mówi:
„Zamiast pisać XPath-y i logikę bezpośrednio w testach, przenieś je do osobnych klas – tzw. Page Objects.”
W praktyce każda taka klasa reprezentuje jedną stronę aplikacji (lub widok), np.
LoginPageHomePageCartPage
Wewnątrz klasy reprezentującej dany widok aplikacji przechowujemy między innymi:
- wszystkie lokatory, zbudowane na bazie np. XPath-ów, CSS-ów i innych selektorów
- metody, które wykonują akcje na tej stronie
- oraz pomocnicze metody wspierające testy
Dzięki temu testy stają się czyste, krótkie i czytelne. Co więcej, POM jest wzorcem uniwersalnym, a ponadto niezależnym od języka programowania.
Struktura projektu
tests/
│
├── test_login.py
├── test_checkout.py
│
pages/
│ ├── base_page.py
│ ├── login_page.py
│ ├── home_page.py
│ └── cart_page.py
│
utils/
│ ├── config_reader.py
│ └── helpers.py
│
conftest.py
pytest.ini
requirements.txt
pages/ – warstwa Page Object
Każdy plik w tym folderze odpowiada jednej stronie lub widokowi aplikacji.
base_page.py
Jest to wspólna klasa bazowa dla wszystkich stron. Zawiera metody, które są uniwersalne i przydatne w każdej podstronie, np.:
class BasePage:
def __init__(self, browser):
self.browser = browser
def click(self, locator):
self.browser.find_element(*locator).click()
def type(self, locator, text):
element = self.browser.find_element(*locator)
element.clear()
element.send_keys(text)
def wait_for(self, locator, timeout=10):
WebDriverWait(self.browser, timeout).until(
EC.visibility_of_element_located(locator)
)
Charakterystyka:
- wspólny punkt dziedziczenia dla innych klas (np.
LoginPage(BasePage)) - ułatwia utrzymanie i rozszerzanie frameworka
- pozwala unikać powtórzeń (DRY)
login_page.py, home_page.py, cart_page.py
Reprezentuje poszczególne strony aplikacji. Każda z tych klas dziedziczy po BasePage oraz definiuje lokatory i metody biznesowe.
class LoginPage(BasePage):
EMAIL_INPUT = (By.ID, "email")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "login-btn")
def login_as(self, email, password):
self.type(self.EMAIL_INPUT, email)
self.type(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
Charakterystyka:
- hermetyzuje logikę interakcji ze stroną
- testy nie wiedzą nic o XPath-ach
- pozwala tworzyć metody semantyczne, np. add_product_to_cart() zamiast click_button(„//button[@id=’add’]”)
utils/ – narzędzia pomocnicze
Zawiera pliki wspierające działanie testów, ale niezwiązane bezpośrednio z logiką stron.
config_reader.py
Odczytuje konfiguracje testów (np. środowisko, URL-e, timeouty).
Może korzystać z plików .ini, .yaml lub .json.
import json
def load_config(path="resources/config.json"):
with open(path) as file:
return json.load(file)
Charakterystyka:
- oddziela dane konfiguracyjne od kodu testów
- ułatwia uruchamianie testów na różnych środowiskach (dev, stage, prod)
- często zawiera dane typu:
base_urlbrowserimplicit_waitapi_endpoint
helpers.py
Zbiór małych funkcji narzędziowych, które są często wykorzystywane w testach lub POM-ach.
def random_email():
import uuid
return f"user_{uuid.uuid4().hex[:6]}@example.com"
def current_timestamp():
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
Charakterystyka:
- Dodatkowo, funkcje wspierające testy (np. generowanie danych czy timestampów) ułatwiają proces automatyzacji
- nie powinna zawierać logiki związanej z Selenium
- dobra praktyka: grupować funkcje wg typu (np. string_utils.py, data_utils.py)
conftest.py
Plik conftest.py to centralne miejsce na konfigurację testów i definicję fixture’ów w pytest.
Dzięki niemu nie musisz powtarzać kodu inicjalizującego przeglądarkę, czytającego dane konfiguracyjne lub ustawiającego środowisko. Pytest automatycznie wykrywa ten plik — nie trzeba go importować ręcznie.
Przykładowy plik:
import pytest
from utils.browser import get_browser
@pytest.fixture
def browser():
driver = get_browser()
yield driver
driver.quit()
Charakterystyka
Plik conftest.py definiuje fixture browser, który tworzy instancję przeglądarki przy użyciu funkcji get_browser() z modułu utils.browser, udostępnia ją testom, a po ich zakończeniu automatycznie zamyka driver (driver.quit()). Taki zabieg sprawia, że mamy czyste środowisko przeglądarkowe i nie musimy martwiwć się o zamykanie przeglądarek.
pytest.ini
pytest.ini to główny plik konfiguracyjny frameworka pytest.
Pozwala ustawić globalne opcje uruchamiania testów, markery, raporty, ścieżki do katalogów czy parametry logowania.
Przykładowy plik:
[pytest]
# Nazwa projektu (pojawi się np. w raportach)
project_name = Test Automation Framework
# Folder, w którym pytest ma szukać testów
testpaths =
tests
# Wzorce nazw plików z testami
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Logowanie
log_cli = true
log_cli_level = INFO
log_file = logs/test_run.log
log_file_level = DEBUG
markers =
smoke: podstawowe testy weryfikujące działanie systemu
regression: testy regresyjne po wdrożeniach
ui: testy interfejsu użytkownika
api: testy API backendu
| Sekcja | Opis |
|---|---|
[pytest] | Każdy plik pytest.ini zaczyna się od tej sekcji |
testpaths | Ścieżka, gdzie pytest ma szukać testów |
python_files, python_classes, python_functions | Wzorce nazw plików, klas i funkcji testowych |
markers | Lista znaczników używanych w projekcie (np. @pytest.mark.smoke) |
addopts | Domyślne opcje uruchamiania pytesta (np. szczegółowość -v, limity błędów) |
log_file | Plik, do którego zapisywane są logi testów |
log_cli | Włącza logi w konsoli podczas uruchamiania testów |
requirements.txt
Spis wszystkich bibliotek wymaganych do uruchomienia testów.
Przykładowy plik:
gherkin-official==29.0.0
parse==1.20.2
parse_type==0.6.4
pytest==8.3.4
pytest-bdd==8.1.0
python-dotenv==1.0.1
requests==2.32.3
selenium==4.28.1
typing_extensions==4.12.2
urllib3==2.3.0
webdriver-manager==4.0.2
pytest-html==4.1.1
Charakterystyka:
- Pozwala szybko odtworzyć środowisko (np. za pomocą
pip install -r requirements.txt) - Dodatkowo, dobrą praktyką jest używanie wersji zablokowanych (
==), a nie otwartych (>=)
Co trzymać w POM, a co w testach ?
W rezultacie warto jasno rozdzielić odpowiedzialności między klasą Page a testem.
Cała struktura staje się dzięki temu bardziej czytelna i łatwiejsza w utrzymaniu:
| W KLASIE PAGE | W TEŚCIE |
|---|---|
Lokatory (By.XPATH, By.CSS_SELECTOR, itp.) | Assercje (sprawdzenie wyniku) |
| Metody: kliknięcie, wpisanie, otwarcie strony | Logika testowa (scenariusz) |
Pomocnicze akcje (np. fill_form, login_as) | Kombinacje kroków |
Warto pamiętać, że testy powinny mówić, co testujesz, a nie jak.
Dzięki temu ich przeglądanie przypomina czytanie książki.
Przykład klasy POM – LoginPage
class LoginPage(BasePage):
"""Strona logowania — dziedziczy po BasePage."""
URL = "https://example.com/login"
def __init__(self, browser: WebDriver) -> None:
super().__init__(browser)
self.email_input = (By.ID, "email")
self.password_input = (By.ID, "passwd")
self.login_button = (By.ID, "SubmitLogin")
self.error_message = (By.XPATH, "//div[contains(@class, 'alert-danger')]")
self.account_section = (By.CLASS_NAME, "account")
def open(self):
"""Otwiera stronę logowania."""
self.browser.get(self.URL)
def enter_email(self, email: str):
"""Wpisuje adres e-mail użytkownika."""
self.type(self.email_input, email)
def enter_password(self, password: str):
"""Wpisuje hasło użytkownika."""
self.type(self.password_input, password)
def click_login(self):
"""Kliknięcie przycisku logowania."""
self.click(self.login_button)
def login_as(self, email: str, password: str):
"""Logowanie użytkownika przy użyciu e-maila i hasła."""
self.enter_email(email)
self.enter_password(password)
self.click_login()
Przykład testu z użyciem POM
import pytest
from assertpy import assert_that
from pages.login_page import LoginPage
@pytest.mark.ui
@pytest.mark.smoke
class TestLoginPositiveCase:
@pytest.mark.regression
def test_login_in_application(self, browser):
"""Test poprawnego logowania do aplikacji."""
login_page = LoginPage(browser)
login_page.open()
login_page.login_as("tester@example.com", "password123")
element = browser.find_element(*login_page.account_section)
assert_that(element.is_displayed()).is_true().described_as(
"Sekcja konta użytkownika powinna być widoczna po zalogowaniu."
)
Zalety PAGE OBJECT MODEL (POM)
Oddzielenie logiki od testów
Dzięki temu testy nie muszą wiedzieć nic o XPath-ach – korzystają jedynie z metod takich jak enter_email() czy click_login().
Łatwe utrzymanie
W przypadku modyfikacji w HTML zmieniasz tylko jeden lokator w LoginPage, zamiast poprawiać go w trzydziestu testach.
Czytelność i semantyka
W praktyce test jest odzwierciedleniem procesu biznesowego:
login_page.open()
login_page.login_as("user", "password")
home_page.open_cart()
cart_page.verify_item_added()
Możliwość rozbudowy
Jeśli chcesz zmodyfikować lub rozszerzyć metody odpowiadające za wykonanie akcji na danej stronie np. sprawdzenie, że strona się wczytała, wystarczy zmodyfikować kod dla danego POM’a.
Nazwy metod, a czytelność
W Page Object Pattern nazwy metod to coś więcej niż tylko etykiety — to język komunikacji między testem a stroną. Dobrze nazwane metody sprawiają, że test czyta się jak scenariusz użytkownika, a nie jak instrukcja dla przeglądarki.
Takie nazwy są semantyczne – mówią co robisz, a nie jak to robisz.
Dzięki temu osoba czytająca test (nawet nietechniczna) od razu rozumie jego cel:
„Otwórz stronę logowania → zaloguj się jako użytkownik → sprawdź wynik.”
Dobrze nazwane metody oznaczają nie tylko mniej komentarzy, ale także większą odporność na zmiany.
Dzięki temu, jeśli zmieni się sposób logowania (np. pojawi się nowy przycisk), testy pozostaną niezmienione – wystarczy zmodyfikować jedną metodę w LoginPage.
Dobre praktyki w projektowaniu POM
- Jedna strona = jedna klasa
W związku z tym nie łącz loginu, koszyka i checkoutu w jednej klasie. - Nazwy metod mówią, co się dzieje
Zamiast click_login_button() lepiej login_as_user(email, password) - Nie umieszczaj assercji w POM
POM to warstwa logiki aplikacji w przeciwieństwie do warstwy walidacji - Wspólne funkcje trzymaj w BasePage
Dzięki temu nie powtarzasz metodclick() - Używaj wzorca Fluent API
Metody mogą zwracać obiekty stron, np.: home_page = login_page.login_as(„user”, „password”)
Dzięki temu test staje się jeszcze bardziej płynny i czytelny
Podsumowanie
Podsumowując, w artykule Page Object Pattern – czyli jak przestać kopiować XPath-y po projekcie został przedstaiony wzorzec Page Object Pattern (POM), jest to must-have dla każdego testera automatyzującego webowe aplikacje.
Dzięki zastosowaniu POM-a można:
- pisać czystsze i bardziej czytelne testy
- unikać duplikacji XPath-ów
- co więcej, szybciej reagować na zmiany w aplikacji
- a także budować skalowalne frameworki testowe
Page Object Pattern reprezentuje poszczególne strony aplikacji.
W rezultacie każda z tych klas dziedziczy po BasePage oraz definiuje lokatory i metody biznesowe, co pozwala utrzymać spójność i czytelność testów nawet w dużych projektach.
👩💻 Autor
Honorata Łyczak
Testerka automatyzująca, pasjonatka jakości oprogramowania.
Specjalizuje się w testach automatycznych, a szczególną sympatią darzy testy wydajnościowe – entuzjastka Locusta 🐛⚡
Linkedin
