Zpět na domů

Playwright: DI a izolace pro 1000 testů

Článek popisuje architekturu pro spuštění 1000+ testů Playwright: gettery v POM pro vyhnutí se state traps, DI přes fixtures s mergeTests, izolaci dat s RUN_ID seedováním Faker a továrnami. Vhodné pro middle/senior vývojáře.

1000 paralelních testů v Playwright bez selhání
Advertisement 728x90

Škálování testů v Playwright: od Page Objects k DI a izolaci dat

V SPA s dynamickým vykreslováním představují konstruktory Page Objects riziko kvůli asynchronní povaze. Pokus o inicializaci stavu v konstruktoru pomocí IIFE vede k podmínkám závodu: test přistupuje k datům dříve, než jsou připravena, což se projeví v CI při paralelním spuštění.

// Špatně: stav v konstruktoru
constructor(page: Page) {
  (async () => {
    this.initialCount = await page.locator('.items').count();
  })();
}

Standardní synchronní inicializace lokátorů je také suboptimální – objekt zachytí snímek DOM v okamžiku vytvoření. Gettery problém řeší pozdním vázáním:

// Správně: gettery pro aktuální DOM
get submitButton() {
  return this.page.getByRole('button', { name: 'Dokončit objednávku' });
}

Tím je zajištěno bezstavové chování: každé volání vidí aktuální DOM, bez vazby na počáteční stav.

Google AdInline article slot

Dependency Injection pomocí fixture v Playwright

Přímé vytváření objektů v testech (new LoginPage(page)) komplikuje refaktorování: změna konstruktoru vyžaduje úpravy v celém projektu. Fixture fungují jako DI kontejner s ukládáním instancí do mezipaměti.

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('dokončení objednávky', async ({ authFlow, checkoutFlow }) => {
  await authFlow.loginAs(user);
  await checkoutFlow.submitOrder();
});

Testy se soustředí na specifikaci, nezávisle na implementaci.

Škálování fixture: mergeTests a Namespacing

Při 20+ fixture se monolitický soubor stává nezvladatelným. Rozdělte je podle domén 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);

Vyhnete se konfliktům názvů pomocí namespacing:

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

test('Administrátor se může přihlásit', async ({ auth }) => {
  await auth.admin.loginAs();
});

Izolace dat při paralelním spouštění

workerIndex nestačí pro CI s rozdělením testů. Použijte kombinaci:

  • RUN_ID z CI (GITHUB_RUN_ID)
  • testId – hash cesty testu
  • repeatEachIndex pro opakované pokusy

Semínko Fakeru tímto složeným klíčem zajišťuje reprodukovatelnost:

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));
  }
});

Lokální spuštění s CI-RUN_ID reprodukuje přesná data selhání.

Vrstvy dat: továrny, přepsání, datové sady

  • Faker: šum pro nedůležitá pole (jména, email)
  • Továrny: struktura s výchozími hodnotami
  • Overrides: kritické změny v testu
  • Datasets: opakující se obchodní případy
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);

Sémantika kroků a ESLint strážci

Oddělte technické kliknutí a obchodní logiku pomocí @Step:

@Step('Dokončení objednávky #${orderId}')
async submitOrder(orderId: string) {
  await this.checkoutPage.fillDetails(orderId);
  await this.checkoutPage.submit();
}

Obchodní krok ('Autorizace') je odolnější vůči změnám UI než technický ('Kliknutí na Přihlásit se').

ESLint zakazuje přímé new PageObject:

{
  "no-restricted-syntax": ["error", {
    "selector": "NewExpression[callee.name=/.*Page$/]",
    "message": "Použijte fixture místo new"
  }]
}

Co je důležité

  • Gettery v POM zajišťují aktuální DOM bez stavových pastí
  • Fixture jako DI minimalizují refaktorování při 1000+ testech
  • RUN_ID + testId nastavuje semínko Fakeru pro stabilitu v CI rozdělení
  • Továrny + datové sady izolují data bez nadbytečného kódu
  • Namespacing fixture zabraňuje kolizím v mergeTests

— Editorial Team

Advertisement 728x90

Číst dál