Wzorce projektowe w Selenium WebDriver to sprawdzone sposoby układania kodu testów: gdzie trzymać selektory, kto tworzy przeglądarkę, jak budować dane testowe i jak dodać logi bez zaśmiecania testów. Automatyzacja testów Selenium bez wzorców działa świetnie przez pierwsze dwadzieścia testów, a potem każda zmiana w UI oznacza poprawki w trzydziestu plikach.
W tym artykule pokazujemy siedem wzorców na jednym małym frameworku w Javie, a nie tylko je opisujemy. Cały kod skompilowaliśmy i uruchomiliśmy 11.10.2026 na Selenium 4.51.0, JUnit 6.1.3 i Javie 25: oba testy przeszły w Chrome 154 w trybie headless. Jeśli Wasza przygoda z Selenium dopiero się zaczyna, najpierw przeczytajcie przewodnik Selenium: co to jest i od czego zacząć, bo tu zakładamy, że pierwszy test macie już za sobą.
Spis treści
- Po co wzorce projektowe w Selenium i automatyzacji testów?
- Jak wygląda Page Object Model w Selenium 4?
- Kiedy wydzielić Page Component Object?
- Jak Factory tworzy WebDrivera dla każdej przeglądarki?
- Dlaczego ThreadLocal zamiast Singletona dla WebDrivera?
- Jak Builder i Strategy porządkują dane i logowanie?
- Jak dodać logi i zrzuty ekranu wzorcem Decorator?
- Jak wygląda test, który składa wzorce w całość?
- Kiedy wzorzec szkodzi zamiast pomagać?
- Jak sprawdzić, że framework Selenium działa?
- Pełna lista źródeł
Po co wzorce projektowe w Selenium i automatyzacji testów?
Test UI dotyka trzech rzeczy, które zmieniają się w różnym tempie: interfejsu, konfiguracji uruchomienia i danych. Gdy wszystkie trzy siedzą w jednej metodzie testowej, każda zmiana w którejkolwiek z nich wymaga edycji testu. Wzorce rozdzielają te odpowiedzialności, tak by zmiana selektora dotykała jednej klasy, a zmiana przeglądarki jednej linijki konfiguracji.
Termin „wzorzec projektowy” spopularyzowała książka „Design Patterns” tak zwanej Bandy Czworga z 1994 roku, która według jej opisu w Wikipedii katalogowała 23 klasyczne wzorce. W testach używamy kilku z nich (Factory, Strategy, Builder, Decorator) oraz wzorców typowych dla automatyzacji UI, takich jak Page Object, który Martin Fowler opisał na swoim blogu 10.09.2013. Poniżej mapa, która pokazuje, jaki problem rozwiązuje każdy z nich.
Źródło: opracowanie Quality Island na podstawie dokumentacji Selenium i katalogu wzorców GoF
Struktura projektu, na którym pracujemy w całym artykule, wygląda tak:
src/test/java/pl/qi/wzorce/
Browser.java enum przeglądarek
DriverFactory.java Factory: tworzy WebDrivera
DriverManager.java ThreadLocal: jeden driver na wątek
LoginPage.java Page Object
DashboardPage.java Page Object złożony z komponentów
HeaderComponent.java Page Component Object
User.java Builder danych testowych (record)
LoginStrategy.java Strategy: interfejs logowania
UiLogin.java Strategy: logowanie przez UI
LoggingListener.java Decorator: logi zdarzeń
LogowanieTest.java test JUnit 6
src/test/resources/aplikacja.htmlJak wygląda Page Object Model w Selenium 4?
Page Object to klasa, która reprezentuje stronę albo jej fragment i udostępnia testom zachowania, a nie elementy. Dokumentacja Selenium podaje dwie główne korzyści: oddzielenie kodu testu od kodu specyficznego dla strony oraz jedno miejsce na usługi i operacje danej strony. Gdy zmieni się selektor przycisku logowania, poprawiacie jedną stałą w LoginPage, a nie dwadzieścia testów.
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class LoginPage {
private static final By EMAIL = By.cssSelector("[data-testid='login-email']");
private static final By PASSWORD = By.cssSelector("[data-testid='login-password']");
private static final By SUBMIT = By.cssSelector("[data-testid='login-submit']");
private static final By ERROR = By.cssSelector("[data-testid='login-error']");
private final WebDriver driver;
private final WebDriverWait wait;
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
}
public DashboardPage loginAs(User user) {
submit(user);
return new DashboardPage(driver);
}
public String loginExpectingError(User user) {
submit(user);
return wait.until(ExpectedConditions.visibilityOfElementLocated(ERROR)).getText();
}
private void submit(User user) {
driver.findElement(EMAIL).sendKeys(user.email());
driver.findElement(PASSWORD).sendKeys(user.password());
driver.findElement(SUBMIT).click();
}
}Zwróćcie uwagę na cztery decyzje w tej klasie:
- Selektory są prywatnymi stałymi typu
By. Test nie wie, że pole e-mail madata-testid, i nie musi wiedzieć - Metody mówią językiem użytkownika:
loginAsiloginExpectingError, a nieclickButtonczyfillInput loginAszwraca następną stronę,DashboardPage. Test czyta się jak scenariusz, a kompilator pilnuje, na której stronie jesteście- W klasie nie ma asercji. Dokumentacja Selenium mówi wprost, że obiekty stron nie powinny same weryfikować wyników, bo to zadanie testu
Mało kto o tym wie
Wiele kursów nadal uczy Page Object z adnotacjami @FindBy i klasą PageFactory. Tymczasem dokumentacja Selenium trzyma opis PageFactory w dziale „Legacy”, w części o Selenium 2, a wszystkie aktualne przykłady Page Object budują lokatory ze zwykłych obiektów By, jak w kodzie wyżej. Leniwie inicjowane proxy z PageFactory potrafią przy tym zwracać nieaktualne elementy na stronach budowanych dynamicznie. PageFactory nie jest zakazane, po prostu jest trochę jak faks w biurze: działa, ale nikt nowy już go nie zamawia.
Konstruktor DashboardPage poniżej czeka na nagłówek przez WebDriverWait z Duration. To dobra praktyka z dokumentacji Selenium: obiekt strony w konstruktorze sprawdza, że strona jest gotowa, i rzuca wyjątek, gdy nie jest. Dlaczego czekamy jawnie, a nie przez implicit wait, wyjaśniamy w artykule Explicit Wait vs Implicit Wait, a jak dobierać stabilne selektory, w tekście selektory w Selenium.
Kiedy wydzielić Page Component Object?
Nagłówek z nazwą użytkownika, menu, modal, tabela z paginacją: te elementy powtarzają się na wielu stronach. Jeśli każdy Page Object obsługuje je po swojemu, wracacie do duplikacji, przed którą Page Object miał chronić. Dokumentacja Selenium nazywa rozwiązanie Page Component Object: obiekt reprezentuje komponent, a nie całą stronę, i szuka elementów tylko wewnątrz swojego korzenia.
public class HeaderComponent {
private final WebElement root;
public HeaderComponent(WebElement root) { this.root = root; }
public String userName() {
return root.findElement(By.cssSelector("[data-testid='user-name']")).getText();
}
public boolean canLogout() {
return root.findElement(By.cssSelector("[data-testid='logout']")).isDisplayed();
}
}public class DashboardPage {
private final HeaderComponent header;
public DashboardPage(WebDriver driver) {
var root = new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("[data-testid='header']")));
this.header = new HeaderComponent(root);
}
public HeaderComponent header() { return header; }
}HeaderComponent dostaje w konstruktorze element korzenia i wywołuje root.findElement, a nie driver.findElement. Dzięki temu selektor user-name nie trafi przypadkiem w identyczny element w stopce. DashboardPage składa się z komponentów, więc gdy panel dostanie drugi komponent, na przykład listę zamówień, dopisujecie jedno pole. Gdy chcecie wspólną klasę bazową dla stron, zobaczcie, jak budujemy BasePage w lekcji o dziedziczeniu w Javie.
Jak Factory tworzy WebDrivera dla każdej przeglądarki?
Factory oddziela tworzenie obiektu od miejsca, w którym się go używa. Test prosi o przeglądarkę, a fabryka decyduje, jakie opcje ustawić. W Javie 25 zapis jest krótki dzięki wyrażeniu switch z yield, które sprawdza kompletność: dodacie do enuma nową przeglądarkę, a kompilator wskaże miejsce, które trzeba uzupełnić.
public enum Browser { CHROME, FIREFOX, EDGE }import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.edge.EdgeOptions;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;
public final class DriverFactory {
private DriverFactory() { }
public static WebDriver create(Browser browser, boolean headless) {
return switch (browser) {
case CHROME -> {
ChromeOptions o = new ChromeOptions();
if (headless) o.addArguments("--headless=new");
yield new ChromeDriver(o);
}
case FIREFOX -> {
FirefoxOptions o = new FirefoxOptions();
if (headless) o.addArguments("-headless");
yield new FirefoxDriver(o);
}
case EDGE -> {
EdgeOptions o = new EdgeOptions();
if (headless) o.addArguments("--headless=new");
yield new EdgeDriver(o);
}
};
}
}W fabryce nie ma System.setProperty ze ścieżką do sterownika. Od Selenium 4.6 sterowniki pobiera Selenium Manager, więc ten krok z dawnych tutoriali można usunąć. Kolejny przypadek fabryki to zdalne uruchomienie: gdy zmienna środowiskowa wskazuje adres Selenium Grid, fabryka zwraca new RemoteWebDriver(url, opcje) zamiast lokalnego sterownika, a testy nie zmieniają się ani o linijkę. Uruchamianie testów w pipeline z takim przełącznikiem budujemy w ramach usługi budowa procesów CI/CD.
Dlaczego ThreadLocal zamiast Singletona dla WebDrivera?
Singleton, czyli jedna statyczna instancja WebDrivera dla całego pakietu, wygląda kusząco: nie trzeba przekazywać drivera między klasami. Problem pojawia się w dniu, w którym włączycie testy równoległe. Wszystkie wątki dostają tę samą przeglądarkę i zaczynają klikać jednocześnie w jednym oknie. Singleton dla WebDrivera to wzorzec, który nie skaluje się dokładnie w tym momencie, w którym framework zaczyna być potrzebny.
public final class DriverManager {
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
private DriverManager() { }
public static void start(WebDriver driver) { DRIVER.set(driver); }
public static WebDriver get() { return DRIVER.get(); }
public static void stop() {
WebDriver d = DRIVER.get();
if (d != null) {
d.quit();
DRIVER.remove();
}
}
}ThreadLocal daje każdemu wątkowi własną przeglądarkę przy tym samym wygodnym dostępie przez DriverManager.get(). Metoda stop() woła remove(), żeby pula wątków nie zostawiła referencji do zamkniętej przeglądarki dla kolejnego testu. Taki układ działa z równoległym uruchamianiem w JUnit 6 i z Selenium Grid, gdzie każdy wątek dostaje osobną sesję na innym węźle.
Jak Builder i Strategy porządkują dane i logowanie?
Dane testowe to druga po selektorach najczęstsza przyczyna zmian w testach. Konstruktor z pięcioma parametrami typu String zaprasza do pomyłki kolejności, a każdy nowy atrybut użytkownika wymaga poprawy wszystkich wywołań. Builder ustawia rozsądne wartości domyślne, a test nadpisuje tylko to, co jest dla niego istotne.
public record User(String email, String password, String role) {
public static Builder builder() { return new Builder(); }
public static final class Builder {
private String email = "tester@example.com";
private String password = "Tajne123!";
private String role = "USER";
public Builder email(String email) { this.email = email; return this; }
public Builder password(String password) { this.password = password; return this; }
public Builder admin() { this.role = "ADMIN"; return this; }
public User build() { return new User(email, password, role); }
}
}W teście błędnego logowania wystarczy User.builder().password("zle").build() i od razu widać, czym ten przypadek różni się od szczęśliwej ścieżki. Rekord Javy daje niezmienność i gotowe equals, więc dane nie zmienią się w połowie testu. Więcej o budowaniu obiektów danych testowych piszemy w lekcji o konstruktorach w Javie.
Strategy pozwala wymienić sposób wykonania kroku bez zmiany testu. Klasyczny przykład to logowanie: test logowania musi przejść przez formularz, ale test koszyka nie musi. Wspólny interfejs i dwie implementacje dają wybór:
public interface LoginStrategy {
DashboardPage login(WebDriver driver, String url, User user);
}public class UiLogin implements LoginStrategy {
@Override
public DashboardPage login(WebDriver driver, String url, User user) {
driver.get(url);
return new LoginPage(driver).loginAs(user);
}
}Druga implementacja, ApiLogin, pobiera token z endpointu logowania, ustawia ciasteczko sesji przez driver.manage().addCookie() i otwiera od razu panel. Nie pokazujemy jej w kodzie, bo zależy od API Waszej aplikacji. Z naszego doświadczenia to jedna z najtańszych optymalizacji pakietu UI: logowanie przez API trwa ułamek czasu formularza, a test formularza zostaje jeden, zamiast powtarzać się na początku każdego scenariusza. Gdy macie już testy API, warto połączyć je z Waszymi testami UI, co robimy w ramach usługi automatyzacja testów API.
Jak dodać logi i zrzuty ekranu wzorcem Decorator?
Logi kroków, zrzuty ekranu przy błędzie, pomiar czasu akcji: to wszystko przydaje się w raporcie, ale wklejone do testów zasłania ich logikę. Wzorzec Decorator owija obiekt w dodatkowe zachowanie bez zmiany jego interfejsu. Selenium 4 ma to wbudowane: EventFiringDecorator zwraca udekorowany WebDriver, który wywołuje Wasz WebDriverListener przed i po każdej akcji.
public class LoggingListener implements WebDriverListener {
final List<String> log = new ArrayList<>();
@Override
public void beforeClick(WebElement element) {
log.add("klik: " + element.getDomAttribute("data-testid"));
}
@Override
public void afterGet(WebDriver driver, String url) {
log.add("otwarto: " + driver.getTitle());
}
}Listener w przykładzie zapisuje każde kliknięcie z identyfikatorem elementu i tytuł każdej otwartej strony. W prawdziwym frameworku w tym samym miejscu robicie zrzut ekranu w metodzie onError i dołączacie go do raportu. Test o tym nie wie, a każdy nowy test dostaje logi za darmo.
Jak wygląda test, który składa wzorce w całość?
Test poniżej używa wszystkich wzorców naraz: fabryka tworzy przeglądarkę, dekorator dokłada logi, menedżer trzyma driver dla wątku, strategia loguje, builder tworzy użytkownika, a obiekty stron ukrywają selektory.
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.events.EventFiringDecorator;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class LogowanieTest {
private static final String URL = Path.of("src/test/resources/aplikacja.html").toUri().toString();
private final LoggingListener listener = new LoggingListener();
private final LoginStrategy login = new UiLogin();
@BeforeEach
void start() {
WebDriver surowy = DriverFactory.create(Browser.CHROME, true);
DriverManager.start(new EventFiringDecorator<WebDriver>(listener).decorate(surowy));
}
@AfterEach
void stop() {
DriverManager.stop();
}
@Test
void poprawneLogowaniePokazujeUzytkownika() {
User user = User.builder().email("anna@example.com").build();
DashboardPage panel = login.login(DriverManager.get(), URL, user);
assertEquals("anna@example.com", panel.header().userName());
assertTrue(panel.header().canLogout());
assertTrue(listener.log.contains("klik: login-submit"));
}
@Test
void bledneHasloPokazujeKomunikat() {
DriverManager.get().get(URL);
String blad = new LoginPage(DriverManager.get())
.loginExpectingError(User.builder().password("zle").build());
assertEquals("Błędny e-mail lub hasło", blad);
}
}Metoda testowa ma trzy linijki przygotowania i trzy asercje. Nie ma w niej żadnego selektora, żadnego sleep ani konfiguracji przeglądarki. Gdy zmieni się formularz logowania w Waszej aplikacji, test się nie zmieni, zmieni się LoginPage. O tym, jak pisać asercje, które mówią coś o wyniku biznesowym, piszemy w tekście o asercjach w testach automatycznych.
Wynik uruchomienia przykładowego frameworka, 11.10.2026
15 z 15
testów z trzech artykułów o Selenium przeszło, w tym 2 z tego frameworka
4,7 s
trwała cała klasa LogowanieTest z dwiema przeglądarkami headless
11
plików w projekcie, żaden nie przekracza 60 linijek
0
selektorów i wywołań sleep w metodach testowych
Źródło: dane własne Quality Island, Maven 3.9.12, Selenium 4.51.0, JUnit 6.1.3, Java 25, Chrome 154
Kiedy wzorzec szkodzi zamiast pomagać?
Każdy wzorzec ma koszt: dodatkową klasę, warstwę abstrakcji, rzecz do wytłumaczenia nowej osobie. W Waszym małym projekcie z dziesięcioma testami Page Object w zupełności wystarczy, a Strategy z jedną implementacją to ozdobnik. Kilka sygnałów, że przesadziliście:
Przerost
Page Object na 800 linijek
Klasa obsługuje cały moduł z setką elementów. Rozbijcie ją na komponenty.
Przerost
Abstrakcja nad abstrakcją
Wrapper na WebElement, który tylko woła click. Usuńcie go, Selenium ma czytelne API.
Niedobór
Asercje w Page Object
Strona sama sprawdza wynik, więc test nie mówi, co weryfikuje. Przenieście asercje do testu.
Niedobór
Statyczny driver
Singleton blokuje równoległość. Zamieńcie go na ThreadLocal, zanim pakiet urośnie.
W bardzo dużych pakietach z wieloma rolami użytkowników niektóre zespoły przechodzą z Page Object na wzorzec Screenplay, w którym test opisuje aktorów, ich zdolności i zadania. Daje to świetną czytelność scenariuszy biznesowych, ale wymaga więcej kodu i dyscypliny. Wybierajcie go, gdy Wasze Page Objecty puchną od metod zależnych od roli, a nie dlatego, że brzmi nowocześnie. Szerszy plan wdrażania automatyzacji, od pierwszych testów po skalowanie, opisuje Strefa QA w tekście plan wdrożenia automatyzacji testów na 6 miesięcy.
Jak sprawdzić, że framework Selenium działa?
Dobry framework widać w liczbach, nie w diagramie klas. Według wpisu na Google Testing Blog z 2016 roku około 1,5% uruchomień testów w Google daje wynik niestabilny, a prawie 16% testów ma jakiś poziom niestabilności, więc nawet najlepsze zespoły mierzą ten wskaźnik stale. U naszych klientów patrzymy na pięć miar:
- Odsetek niestabilnych uruchomień: testy, które przy tym samym kodzie raz przechodzą, raz nie
- Czas pełnej regresji w pipeline i jego trend z miesiąca na miesiąc
- Liczba plików zmienianych przy zmianie UI: przy dobrym Page Object to jeden, najwyżej dwa
- Czas analizy czerwonego buildu: logi i zrzuty z dekoratora skracają go najbardziej
- Błędy wykryte przed produkcją względem tych, które zgłosili użytkownicy
W projekcie dla Argos automatyzacja testów i uporządkowanie procesów QA skróciły regresję przed releasem z 5 dni do 10 godzin, a błędów krytycznych na produkcji było o 46% mniej, co opisujemy w case study Argos. Według danych Quality Island nasze zespoły napisały ponad 450 000 testów automatycznych i to doświadczenie stoi za wzorcami z tego artykułu.
Wzorce z tego artykułu, od Page Object po równoległe uruchomienia, ćwiczymy krok po kroku na szkoleniu Automatyzacja testów Selenium WebDriver, a podstawy języka, na których stoją, na szkoleniu Programowanie w języku Java dla testerów. Gdy Wasz framework urósł bez architektury, pomoże Wam doradztwo w automatyzacji testów: przegląd kodu, plan refaktoryzacji i wsparcie zespołu przy jej wdrożeniu.
Co zabrać z tego artykułu
01Page Object ukrywa selektory i udostępnia zachowania. Nie zawiera asercji, a metody zwracają kolejne strony.
02Powtarzalne fragmenty UI modelujcie jako Page Component Object, który szuka elementów tylko w swoim korzeniu.
03Factory tworzy przeglądarkę i przełącza na Selenium Grid bez zmiany testów. Selenium Manager zastępuje System.setProperty.
04Zamiast Singletona używajcie ThreadLocal, inaczej testy równoległe będą klikać w jednym oknie.
05Builder daje czytelne dane testowe, Strategy pozwala logować przez UI albo przez API.
06Logi i zrzuty ekranu dodajcie dekoratorem EventFiringDecorator, a nie w każdym teście.
Źródło: dokumentacja Selenium 2026, framework przykładowy Quality Island z 11.10.2026
Wasz framework Selenium urósł bez architektury i każda zmiana w UI boli? Zrobimy przegląd i plan refaktoryzacji.
Powiązane na blogu Quality Island
- Explicit Wait vs Implicit Wait w Selenium 4: kod z Duration i pomiar czasu
- Selektory w Selenium: 8 lokatorów, XPath, CSS i lokatory względne z kodem
- Selenium: co to jest, jak działa i od czego zacząć automatyzację testów
- Konstruktory w Javie: new, this(), Page Object i builder danych testowych
- Jak skrócić testy regresyjne z 5 dni do 10 godzin: case Argos
Pełna lista źródeł
- Selenium, dokumentacja „Page object models”, w tym Page Component Objects, dostęp 11.10.2026
- M. Fowler, „PageObject”, martinfowler.com, 10.09.2013
- Selenium, dokumentacja Legacy, „PageFactory” (Selenium 2), dostęp 11.10.2026
- Selenium, dokumentacja „WebDriver Listeners” (EventFiringDecorator), dostęp 11.10.2026
- Selenium, dokumentacja „Grid”, dostęp 11.10.2026
- Wikipedia, „Design Patterns” (Gamma, Helm, Johnson, Vlissides, 1994), dostęp 11.10.2026
- J. Micco, „Flaky Tests at Google and How We Mitigate Them”, Google Testing Blog, 2016
- Selenium, „Announcing Selenium 4”, blog projektu, 13.10.2021
- Quality Island, framework przykładowy: Selenium 4.51.0, JUnit 6.1.3, Java 25, Chrome 154 headless, uruchomiony 11.10.2026
- Quality Island, case study Argos, dane potwierdzone przez klienta



