Powrót do strony głównej

Playwright: DI i izolacja dla 1000 testów

Artykuł opisuje architekturę do uruchamiania 1000+ testów Playwright: gettery w POM do uniknięcia pułapek stanu, DI przez fikstury z mergeTests, izolację danych z seedowaniem RUN_ID Faker i fabrykami. Nadaje się dla deweloperów middle/senior.

1000 równoległych testów w Playwright bez awarii
Advertisement 728x90

Skalowanie testów Playwright: od Page Objects do DI i izolacji danych

W SPA z dynamicznym renderowaniem konstruktory Page Objects stwarzają ryzyka z powodu asynchroniczności. Próba inicjalizacji stanu w konstruktorze za pomocą IIFE prowadzi do warunków wyścigu: test odwołuje się do danych przed ich gotowością, co ujawnia się w CI przy równoległym uruchamianiu.

// Niepoprawnie: stan w konstruktorze
constructor(page: Page) {
  (async () => {
    this.initialCount = await page.locator('.items').count();
  })();
}

Standardowa synchroniczna inicjalizacja lokalizatorów też jest suboptymalna — obiekt utrwala migawkę DOM w momencie tworzenia. Gettery rozwiązują problem późnym wiązaniem:

// Poprawnie: gettery dla aktualnego DOM
get submitButton() {
  return this.page.getByRole('button', { name: 'Złóż zamówienie' });
}

Zapewnia to zachowanie bezstanowe: każde wywołanie widzi bieżący DOM, bez przywiązania do stanu początkowego.

Google AdInline article slot

Dependency Injection poprzez fixture Playwright

Bezpośrednie tworzenie obiektów w testach (new LoginPage(page)) utrudnia refaktoryzację: zmiana konstruktora wymaga poprawek w całym projekcie. Fixture pełnią rolę kontenera DI z buforowaniem instancji.

export const test = base.extend({
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },
  checkoutFlow: async ({ cartPage, checkoutPage }, use) => {
    await use(new CheckoutFlow(cartPage, checkoutPage));
  }
});

test('składanie zamówienia', async ({ authFlow, checkoutFlow }) => {
  await authFlow.loginAs(user);
  await checkoutFlow.submitOrder();
});

Testy skupiają się na specyfikacji, niezależnie od implementacji.

Skalowanie fixture: mergeTests i Namespacing

Przy 20+ fixture plik monolityczny staje się niezarządzalny. Podziel je według domen za pomocą mergeTests:

Google AdInline article slot
// auth.fixtures.ts
export const authTest = base.extend({ ... });

// fixtures.ts
import { mergeTests } from '@playwright/test';
export const test = mergeTests(authTest, cartTest);

Unikaj konfliktów nazw poprzez namespacing:

export const test = base.extend({
  auth: async ({ page }, use) => {
    await use({ admin: new Admin(page), user: new User(page) });
  }
});

test('Administrator może się zalogować', async ({ auth }) => {
  await auth.admin.loginAs();
});

Izolacja danych w równoległych uruchomieniach

workerIndex jest niewystarczający dla CI z podziałem na fragmenty. Użyj kombinacji:

  • RUN_ID z CI (GITHUB_RUN_ID)
  • testId — hash ścieżki testu
  • repeatEachIndex dla ponownych prób

Ziarno Faker z tym kluczem złożonym zapewnia powtarzalność:

Google AdInline article slot
export function seedFaker(testInfo: TestInfo) {
  const RUN_ID = process.env.RUN_ID || 'local';
  const seed = hashCode(`${testInfo.testId}-${RUN_ID}`);
  faker.seed(seed);
  return faker;
}

export const test = base.extend({
  faker: async ({}, use, testInfo) => {
    await use(seedFaker(testInfo));
  }
});

Lokalne uruchomienie z CI-RUN_ID odtwarza dokładne dane błędów.

Warstwy danych: fabryki, nadpisywania, zestawy danych

  • Faker: szum dla nieistotnych pól (imiona, email)
  • Fabryki: struktura z domyślnymi wartościami
  • Overrides: krytyczne zmiany w teście
  • Datasets: powtarzające się przypadki biznesowe
export function createUser(overrides?: Partial<User>, f = faker): User {
  return {
    id: f.string.uuid(),
    name: f.person.fullName(),
    role: 'customer',
    ...overrides
  };
}

export const VIP_USER = { role: 'vip', discount: 0.15 };

Test: const vipUser = createUser({ ...VIP_USER }, faker);

Semantyka kroków i strażnicy ESLint

Rozdzielaj techniczne kliknięcia i logikę biznesową za pomocą @Step:

@Step('Składanie zamówienia #${orderId}')
async submitOrder(orderId: string) {
  await this.checkoutPage.fillDetails(orderId);
  await this.checkoutPage.submit();
}

Krok biznesowy ('Autoryzacja') jest bardziej odporny na zmiany UI niż techniczny ('Kliknij Zaloguj').

ESLint zabrania bezpośrednich new PageObject:

{
  "no-restricted-syntax": ["error", {
    "selector": "NewExpression[callee.name=/.*Page$/]",
    "message": "Użyj fixture zamiast new"
  }]
}

Co jest ważne

  • Gettery w POM zapewniają aktualny DOM bez pułapek stanu
  • Fixture jako DI minimalizują refaktoryzację przy 1000+ testach
  • RUN_ID + testId ustawia ziarno Faker dla stabilności w fragmentach CI
  • Fabryki + zestawy danych izolują dane bez nadmiarowego kodu
  • Namespacing fixture zapobiega kolizjom w mergeTests

— Editorial Team

Advertisement 728x90

Czytaj dalej