Zurück zur Startseite

Playwright: DI und Isolation für 1000 Tests

Der Artikel beschreibt die Architektur für das Ausführen von 1000+ Playwright Tests: Getter in POM zur Vermeidung von State Traps, DI durch Fixtures mit mergeTests, Data Isolation mit RUN_ID Seeding von Faker und Factories. Geeignet für Middle/Senior-Entwickler.

1000 parallele Tests in Playwright ohne Ausfälle
Advertisement 728x90

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.

Google AdInline article slot

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:

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

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:

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

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

Advertisement 728x90

Weiterlesen