Playwright-Tests skalieren: Von Page Objects zu Dependency Injection und Datenisolation
In SPAs mit dynamischem Rendering bergen Page-Object-Konstruktoren Risiken durch Asynchronität. Der Versuch, den Zustand im Konstruktor über eine IIFE zu initialisieren, führt zu Race Conditions: Der Test greift auf Daten zu, bevor sie bereit sind, was sich im CI bei parallelen Läufen manifestiert.
// Falsch: Zustand im Konstruktor
constructor(page: Page) {
(async () => {
this.initialCount = await page.locator('.items').count();
})();
}
Auch die Standardinitialisierung synchroner Locatoren ist suboptimal – das Objekt erfasst beim Erstellen einen DOM-Snapshot. Getter lösen das Problem mit Late Binding:
// Richtig: Getter für aktuellen DOM
get submitButton() {
return this.page.getByRole('button', { name: 'Bestellung aufgeben' });
}
Dies gewährleistet zustandsloses Verhalten: Jeder Aufruf sieht den aktuellen DOM, ohne an den Ausgangszustand gebunden zu sein.
Dependency Injection über Playwright-Fixtures
Die direkte Objekterstellung in Tests (new LoginPage(page)) erschwert Refactoring: Eine Änderung des Konstruktors erfordert Anpassungen im gesamten Projekt. Fixtures fungieren als DI-Container mit Instanz-Caching.
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('Bestellung aufgeben', async ({ authFlow, checkoutFlow }) => {
await authFlow.loginAs(user);
await checkoutFlow.submitOrder();
});
Tests konzentrieren sich auf Spezifikationen, unabhängig von der Implementierung.
Fixtures skalieren: mergeTests und Namespacing
Bei 20+ Fixtures wird eine monolithische Datei unübersichtlich. Nach Domänen aufteilen mit mergeTests:
// auth.fixtures.ts
export const authTest = base.extend({ ... });
// fixtures.ts
import { mergeTests } from '@playwright/test';
export const test = mergeTests(authTest, cartTest);
Namenskonflikte durch Namespacing vermeiden:
export const test = base.extend({
auth: async ({ page }, use) => {
await use({ admin: new Admin(page), user: new User(page) });
}
});
test('Admin kann sich anmelden', async ({ auth }) => {
await auth.admin.loginAs();
});
Datenisolation bei parallelen Läufen
workerIndex reicht für CI mit Shards nicht aus. Eine Kombination verwenden:
- RUN_ID aus dem CI (z.B. GITHUB_RUN_ID)
- testId – Hash des Testpfads
- repeatEachIndex für Wiederholungen
Faker mit diesem zusammengesetzten Schlüssel zu seeden, gewährleistet Reproduzierbarkeit:
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));
}
});
Lokale Läufe mit CI-RUN_ID reproduzieren exakt die Daten von Fehlern.
Datenschichten: Factories, Overrides, Datasets
- Faker: Rauschen für nicht-kritische Felder (Namen, E-Mail)
- Factories: Struktur mit Standardwerten
- Overrides: Kritische Änderungen in Tests
- Datasets: Wiederkehrende Geschäftsfälle
export function createUser(overrides?: Partial<User>, f = faker): User {
return {
id: f.string.uuid(),
name: f.person.fullName(),
role: 'kunde',
...overrides
};
}
export const VIP_USER = { role: 'vip', discount: 0.15 };
Test: const vipUser = createUser({ ...VIP_USER }, faker);
Schritt-Semantik und ESLint-Guards
Technische Klicks von Geschäftslogik mit @Step trennen:
@Step('Bestellung #${orderId} aufgeben')
async submitOrder(orderId: string) {
await this.checkoutPage.fillDetails(orderId);
await this.checkoutPage.submit();
}
Geschäftsschritte ('Anmelden') sind resilienter gegenüber UI-Änderungen als technische ('Auf Anmelde-Button klicken').
ESLint verbietet direkte new PageObject:
{
"no-restricted-syntax": ["error", {
"selector": "NewExpression[callee.name=/.*Page$/]",
"message": "Fixtures statt new verwenden"
}]
}
Wichtige Erkenntnisse
- Getter im POM gewährleisten aktuellen DOM ohne Zustandsfallen
- Fixtures als DI minimieren Refactoring bei 1000+ Tests
- RUN_ID + testId seedet Faker für Stabilität in CI-Shards
- Factories + Datasets isolieren Daten ohne redundanten Code
- Namespacing von Fixtures verhindert Kollisionen in mergeTests
— Editorial Team
Noch keine Kommentare.