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.
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:
// 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ść:
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
Brak komentarzy.