Logo Light
Published on

Playwright - Lazy Gettery Page Object Modelu

Playwright - Inicializace lokátorů v Page Object Modelu

Při psaní testů v Playwrightu je běžné používat Page Object Model (POM), což je návrhový vzor pro organizaci testovací logiky a lokátorů v samostatných třídách. Je vhodné při použití POM v Playwrightu inicializovat všechny lokátory přímo v konstruktoru třídy? V dokumentaci Playwrightu je uveden příklad použití POM, kde inicializují všechny lokátory v konstruktoru - Playwright POM dokumentace. Jaké má tento přístup výhody a nevýhody? A jaká existuje alternativa?

Pozn.: V Playwrightu jsou lokátory vždy “lazy” – ať je vytvoříme v konstruktoru, nebo přes getter. Vytvoření lokátoru neprovádí žádné vyhledávání v DOM; k reálnému hledání dojde až při použití (klik, čtení textu, expect(...)).

Výhody inicializace všech lokátorů v konstruktoru

  • přehlednost: hned vidím, s kterými lokátory může page object pracovat
  • centralizace: všechny lokátory na jednom místě

Nevýhody

  • zbytečná inicializace (alokace paměti) v případě, že test daný lokátor nepoužije
  • problémy u dynamických prvků – pokud stránka obnoví obsah (např. iframe, reload sekce stránky), lokátor uložený v konstruktoru může ukazovat na starý kontext

Tyto nevýhody můžeme odstranit, když lokátory nebudeme inicializovat v konstruktoru, ale použijeme lazy gettery. Lokátory se tak inicializují pouze tehdy, pokud je skutečně použijeme.

Inicializace lokátorů v konstruktoru

import { Locator, Page, expect } from '@playwright/test'

export class ListOfUsers {
  private readonly idItems: Locator
  private readonly nameItems: Locator
  private readonly emailItems: Locator
  private readonly userItems: Locator

  constructor(private page: Page) {
    this.idItems = page.getByTestId('id-item')
    this.nameItems = page.getByTestId('name-item')
    this.emailItems = page.getByTestId('email-item')
    this.userItems = page.getByTestId('user-item')
  }

  public async expectUserCount(count: number): Promise<void> {
    await expect(this.userItems).toHaveCount(count)
  }
}

Použití lazy getterů místo konstruktoru

import { Page, expect } from '@playwright/test'

export class ListOfUsers {
  constructor(private page: Page) {}

  get idItems() {
    return this.page.getByTestId('id-item')
  }

  get nameItems() {
    return this.page.getByTestId('name-item')
  }

  get emailItems() {
    return this.page.getByTestId('email-item')
  }

  get userItems() {
    return this.page.getByTestId('user-item')
  }

  public async expectUserCount(count: number): Promise<void> {
    await expect(this.userItems).toHaveCount(count)
  }
}

Použití lazy getterů v testu

Představme si jednoduchý test, který ověřuje počet zobrazených uživatelů na stránce. Pomocí instance třídy ListOfUsers můžeme přistupovat k metodě expectUserCount, která interně využívá getter userItems. Díky tomu se lokátor user-item vytvoří až ve chvíli, kdy ho opravdu potřebujeme – v tomto případě během provádění assertu (expect).

import { test, expect } from '@playwright/test'
import { ListOfUsers } from '../page-objects/ListOfUsers'

test('should display the correct number of users', async ({ page }) => {
  await page.goto('https://example.com/users')

  const listOfUsers = new ListOfUsers(page)

  await listOfUsers.expectUserCount(5)
})

Getter userItems v pozadí zavolá this.page.getByTestId('user-item'), ale až tehdy, když k němu v metodě expectUserCount přistoupíme. Tím se vyhneme zbytečné inicializaci lokátorů, které test vůbec nevyužije. Výsledkem je přehlednější konstruktor a efektivnější práce s pamětí.

Shrnutí

Inicializace lokátorů v konstruktoru nabízí přehlednost a centralizaci, ale může vést ke zbytečné inicializaci nevyužitých prvků. V Playwrightu jsou Locator objekty vždy lazy – samotné vytvoření neprohledává DOM. Rozdíl mezi konstruktorem a getterem je v okamžiku vytvoření tohoto objektu:

  • Konstruktor – lokátor je vytvořen při instanciaci POM. Hodí se pro stabilní stránky, kde se struktura nemění a chceme mít všechny lokátory hned pohromadě.
  • Getter – lokátor se vytvoří až při použití. To je užitečné u dynamických scénářů (reloady, měnící se iframy) a udržuje konstruktor čistý. V praxi můžeš oba přístupy kombinovat – statické prvky v konstruktoru, dynamické přes gettery.