Przejdź do treści
    POM - czym jest i czym nie jest (i dlaczego połowa robi to źle)

    POM - czym jest i czym nie jest (i dlaczego połowa robi to źle)

    Marzec 2, 2026
    12 min czytania

    POM - czym jest i czym nie jest (i dlaczego połowa robi to źle)

    Page Object Model to prawdopodobnie najczęściej dyskutowany wzorzec w testowaniu automatycznym. Jedni go uwielbiają, inni twierdzą że jest przestarzały. Moim zdaniem obie strony tej dyskusji mają rację - tyle że rozmawiają o dwóch zupełnie różnych implementacjach.

    Zanim przejdę do konkretów: to jest moje zdanie oparte na doświadczeniu. Wzorzec nie jest dobry ani zły sam w sobie - sposób implementacji robi różnicę.

    Czym POM jest, a czym nie jest

    POM to wzorzec hermetyzujący wiedzę o tym, jak znaleźć elementy na stronie i jak z nimi interagować. Kiedy strona zmienia swój układ albo dodaje nowe pola - idziesz do jednego pliku i poprawiasz. Nie szukasz locatorów rozrzuconych po 20 plikach testowych.

    POM nie jest miejscem na logikę testów. To co testujemy, co sprawdzamy i w jakiej kolejności - to należy do testu, nie do Page Objecta.

    Brzmi prosto. Ale tu właśnie większość implementacji zbacza.

    Skąd się biorą złe POM-y

    Ludzie nie wymyślają złych POM-ów sami. Uczą się ich.

    Większość tutoriali - zarówno starych kursów Selenium, jak i wielu współczesnych materiałów na YouTube czy Udemy - uczy POM jako "opakuj locatory w metody klasy". Krok 1: stwórz klasę. Krok 2: dodaj pole na każdy element. Krok 3: napisz metodę na każdą interakcję. Gotowe, masz Page Object.

    Problem w tym, że to uczy myślenia o POM od strony struktury UI, nie od strony akcji użytkownika. Efekt? Ludzie tworzą klasy, które są lustrzanym odbiciem strony HTML - jedno pole na każdy input, jeden klik na każdy przycisk. Technicznie poprawne. Praktycznie bezużyteczne.

    Najczęstsze błędy, które z tego wynikają:

    Błąd #1: granularne metody

    Najczęstszy błąd - POM z metodami odzwierciedlającymi każde pojedyncze kliknięcie:

    TYPESCRIPT
    // Zły POM - granularne, niskopoziomowe metody
    class BookingPage {
    async clickMovieDune() {
    await this.page.getByRole('listitem')
    .filter({ hasText: 'Dune: Part Three' })
    .getByRole('button', { name: 'Wybierz seans' })
    .click();
    }
    async clickShowtime1830() {
    await this.page.getByRole('radio', { name: '18:30 - sala 2' }).click();
    }
    async clickSeatD7() {
    await this.page.getByTestId('seat-D7').click();
    }
    async clickConfirmSeats() {
    await this.page.getByRole('button', { name: 'Zatwierdź miejsca' }).click();
    }
    async fillName(name: string) {
    await this.page.getByLabel('Imię i nazwisko').fill(name);
    }
    // ... i tak dla każdego elementu
    }

    A potem test wygląda tak:

    TYPESCRIPT
    // Test z granularnym POM - lista kroków bez kontekstu
    test('rezerwacja biletu', async ({ page }) => {
    const booking = new BookingPage(page);
    await booking.clickMovieDune();
    await booking.clickShowtime1830();
    await booking.clickSeatD7();
    await booking.clickConfirmSeats();
    await booking.fillName('Jan Kowalski');
    await booking.fillEmail('[email protected]');
    await booking.fillPhone('600 123 456');
    await booking.clickSubmit();
    // ...
    });

    Co to nam dało? Test jest dłuższy i trudniejszy w czytaniu niż bez POM. Teraz skaczemy między dwoma plikami zamiast jednego. Każda zmiana w UI wciąż wymaga poprawki - tyle że w innym pliku.

    To nie jest POM. To jest Locator Repository w przebraniu POM-a.

    Rozwiązanie: metody na poziomie biznesowym

    Dobry POM mówi "co użytkownik robi", nie "w co klika":

    TYPESCRIPT
    // Dobry POM - deklaratywne, biznesowe akcje
    class KinoTestPage {
    constructor(private page: Page) {}
    async goto() {
    await this.page.goto('https://kinotest.howtest.it');
    }
    async selectMovie(title: string) {
    await this.page.getByRole('listitem')
    .filter({ hasText: title })
    .getByRole('button', { name: 'Wybierz seans' })
    .click();
    }
    async selectShowtime(time: string) {
    await this.page.getByRole('radio', { name: time }).click();
    await this.page.getByRole('button', { name: 'Dalej' }).click();
    }
    async selectSeat(seatId: string) {
    await this.page.getByTestId('seat-' + seatId).click();
    await this.page.getByRole('button', { name: 'Zatwierdź miejsca' }).click();
    }
    async fillBookingDetails(name: string, email: string, phone: string) {
    await this.page.getByLabel('Imię i nazwisko').fill(name);
    await this.page.getByLabel('Adres e-mail').fill(email);
    await this.page.getByLabel('Numer telefonu').fill(phone);
    await this.page.getByRole('button', { name: 'Zarezerwuj' }).click();
    }
    async getBookingNumber(): Promise<string> {
    return await this.page.getByTestId('booking-number').innerText();
    }
    }

    Test z takim POM:

    TYPESCRIPT
    test('rezerwacja biletu - happy path', async ({ page }) => {
    const kino = new KinoTestPage(page);
    await kino.goto();
    await kino.selectMovie('Dune: Part Three');
    await kino.selectShowtime('18:30 - sala 2');
    await kino.selectSeat('D7');
    await kino.fillBookingDetails('Jan Kowalski', '[email protected]', '600 123 456');
    const bookingNumber = await kino.getBookingNumber();
    expect(bookingNumber).toMatch(/^KN-/);
    });

    Ten test możesz pokazać product ownerowi i powie "tak, to dokładnie to co chciałem przetestować". Bez znajomości Playwright.

    Kluczowe pytanie przy projektowaniu POM: czy ta metoda opisuje akcję użytkownika ('zarezerwuj bilet') czy implementację UI ('kliknij przycisk zatwierdź')? Jeśli drugie - to prawdopodobnie zły poziom abstrakcji.

    Błąd #2: asercje w Page Objectach

    Drugi częsty błąd - wrzucanie asercji do POM:

    TYPESCRIPT
    // Źle - POM z asercjami
    class BookingPage {
    async fillBookingDetails(name: string, email: string, phone: string) {
    await this.page.getByLabel('Imię i nazwisko').fill(name);
    await this.page.getByLabel('Adres e-mail').fill(email);
    await this.page.getByLabel('Numer telefonu').fill(phone);
    await this.page.getByRole('button', { name: 'Zarezerwuj' }).click();
    // To nie powinno tu być
    await expect(this.page.getByTestId('booking-number')).toBeVisible();
    await expect(this.page.getByText('Rezerwacja potwierdzona')).toBeVisible();
    }
    }

    Dlaczego to problem? Bo teraz każdy test używający fillBookingDetails() automatycznie sprawdza sukces rezerwacji. A co jeśli chcę przetestować scenariusz, w którym rezerwacja powinna się nie udać? Muszę napisać osobną metodę albo obchodzić istniejącą.

    POM odpowiada za akcje - "wypełnij formularz i kliknij zarezerwuj". Test odpowiada za weryfikację - "sprawdź że rezerwacja się udała" albo "sprawdź że wyświetlił się błąd walidacji".

    TYPESCRIPT
    // Dobrze - asercje w teście
    test('rezerwacja z pustym mailem pokazuje błąd', async ({ page }) => {
    const kino = new KinoTestPage(page);
    await kino.goto();
    await kino.selectMovie('Dune: Part Three');
    await kino.selectShowtime('18:30 - sala 2');
    await kino.selectSeat('D7');
    await kino.fillBookingDetails('Jan Kowalski', '', '600 123 456');
    // Asercja tu - w teście, nie w POM
    await expect(page.getByText('Podaj adres e-mail')).toBeVisible();
    });

    Jest jeden wyjątek: waity na nawigację. Jeśli metoda POM powoduje przejście na nową stronę, sensowne jest poczekanie na załadowanie tej strony wewnątrz metody - to nie jest asercja testowa, to część akcji.

    Błąd #3: dziedziczenie zamiast kompozycji

    Klasyk z Javowego świata Selenium - BasePage, z którego dziedziczy wszystko:

    TYPESCRIPT
    // Źle - hierarchia dziedziczenia
    class BasePage {
    constructor(protected page: Page) {}
    async waitForPageLoad() { /* ... */ }
    async getHeader() { /* ... */ }
    async getFooter() { /* ... */ }
    async acceptCookies() { /* ... */ }
    }
    class BookingPage extends BasePage {
    async selectMovie(title: string) { /* ... */ }
    async selectShowtime(time: string) { /* ... */ }
    }
    class VIPBookingPage extends BookingPage {
    async selectVIPLounge() { /* ... */ }
    async addDrinkPackage() { /* ... */ }
    }

    Na początku wygląda elegancko. Ale po kilku miesiącach BasePage staje się śmietnikiem - każda "wspólna" metoda ląduje w nim, bo "wszystkie strony tego potrzebują". A VIPBookingPage dziedziczy metody selectShowtime() i acceptCookies(), których może w ogóle nie używa.

    Lepiej: osobne, małe Page Objecty składane w teście przez kompozycję.

    TYPESCRIPT
    // Dobrze - kompozycja zamiast dziedziczenia
    class CookieBanner {
    constructor(private page: Page) {}
    async accept() {
    await this.page.getByRole('button', { name: 'Akceptuję' }).click();
    }
    }
    class MovieList {
    constructor(private page: Page) {}
    async selectMovie(title: string) { /* ... */ }
    }
    class SeatPicker {
    constructor(private page: Page) {}
    async selectSeat(seatId: string) { /* ... */ }
    }
    // Test składa to co potrzebuje
    test('rezerwacja VIP', async ({ page }) => {
    const cookies = new CookieBanner(page);
    const movies = new MovieList(page);
    const seats = new SeatPicker(page);
    await cookies.accept();
    await movies.selectMovie('Dune: Part Three');
    await seats.selectSeat('VIP-A1');
    });

    Każdy POM jest mały, niezależny i robi jedną rzecz. Nie ciągniesz za sobą całego drzewa dziedziczenia.

    Co się dzieje przy zmianie UI

    Teoria teorią - zobaczmy co się dzieje w praktyce. Załóżmy, że formularz rezerwacji się zmienia: zamiast trzech osobnych pól (imię, email, telefon) jest teraz logowanie przez Google i jedno pole na telefon.

    Z granularnym POM - poprawiasz fillName(), fillEmail(), usuwasz je, dodajesz clickGoogleLogin(), a potem poprawiasz każdy test który wołał te metody po kolei. 15 testów, 15 poprawek.

    Z biznesowym POM - poprawiasz wnętrze fillBookingDetails(). Metoda nadal przyjmuje dane kontaktowe i finalizuje rezerwację - zmienia się tylko implementacja w środku. Testy się nie zmieniają, bo nie wiedzą jak formularz wygląda. Wiedzą tylko, że podają dane i rezerwują.

    To jest ta praktyczna różnica, dla której warto robić POM dobrze.

    Co POM robi dla Ciebie w praktyce

    Jedno miejsce do poprawki przy zmianie UI. Kiedy siatka miejsc dostała nową strukturę i data-testid zmienił format - poprawiasz metodę selectSeat() w jednym miejscu. Wszystkie testy używające tej metody działają dalej bez zmian.

    Reużywalność. fillBookingDetails() możesz używać w testach happy path, testach walidacji, testach powiadomień email. Piszesz raz.

    Czytelność. Test opisuje scenariusz biznesowy. Ktoś nowy w projekcie czyta test i rozumie co sprawdza bez zagłębiania się w locatory.

    Gdzie POM ma swoje granice

    Jest kilka sytuacji, gdzie POM może być overkill:

    Małe projekty - jeśli masz 20 testów i jedną stronę, POM dodaje złożoność bez realnej korzyści. Locatory można trzymać bezpośrednio w testach.

    Szybkie eksperymenty - piszesz testy do jednorazowego sprawdzenia, nie do długoterminowego utrzymania. Nie ma sensu tworzyć Page Objectów.

    Functional helpers jako alternatywa - zamiast klas, możesz używać zwykłych funkcji eksportowanych z modułu. To ta sama filozofia (hermetyzacja akcji biznesowych), tylko bez klas. Oba podejścia są ok.

    Moje podejście

    Kiedy zaczynam nowy projekt testowy, zaczynam bez POM. Piszę kilka testów bezpośrednio. Po pewnym czasie zaczynam widzieć powtarzające się bloki kodu - np. "przejście do siatki miejsc" pojawia się w 10 testach. Wtedy wyodrębnia się naturalnie do Page Objecta.

    POM, który powstaje po refaktoryzacji realnych, istniejących testów, jest prawie zawsze lepszy niż POM zaprojektowany abstrakcyjnie na początku projektu.

    Checklist dobrego POM

    Projektowanie

    Metody opisują akcje użytkownika, nie implementację UI
    Nazwy metod są czytelne bez znajomości kodu: selectMovie(), not clickMovieCard()

    Elastyczność

    POM przyjmuje parametry - nie hardcoduje danych testowych

    Separacja

    Asercje są w testach, nie w Page Objectach (z małymi wyjątkami)

    Architektura

    Nie używasz dziedziczenia POM - kompozycja zamiast hierarchii klas

    Podejście

    POM powstał przez refaktoryzację istniejących testów, nie z góry

    Podsumowanie

    Trzy najczęstsze błędy w POM:

    • Granularne metody - opakowują kliknięcia zamiast akcji biznesowych
    • Asercje w POM - blokują reużywalność i mieszają odpowiedzialności
    • Dziedziczenie - tworzy kruche hierarchie zamiast elastycznych komponentów

    Wszystkie wynikają z tego samego źródła: myślenia o POM od strony struktury UI, nie od strony tego, co użytkownik robi. Jeśli Twoje metody POM czytają się jak scenariusz biznesowy - jesteś na dobrej drodze.

    Podobał Ci się wpis?

    Zapisz się do newslettera, a dam Ci znać o nowych wpisach i narzędziach QA.

    Zapisz się do newslettera