Styrow.dev
Question 18 of 28

Designing a resilient test harness for concurrent, stateful E2E scenarios in Playwright

🔥 Playwright Hard Mode: Conquering Concurrent Stateful E2E Tests! Scenario: Building a large-scale Playwright E2E suite where hundreds of tests run concurrently, each needing a unique, isolated, and complex backend state (e.g., specific user orders, inventory levels). How do you architect for true isolation, rapid state provisioning, and robust cleanup?

📌 Problem Statement Running concurrent, stateful E2E tests presents significant challenges for reliable CI/CD feedback. Cross-test contamination due to shared backend resources leads to flaky, hard-to-debug failures. ❌ Relying solely on Playwright’s browser context isolation is insufficient for backend state. ✅ We need a production-grade strategy for managing external state, ensuring each test worker operates on a clean, dedicated environment.

đź’ˇ Solution & Code Walkthrough

• Isolation Strategy: Ephemeral Environments per Worker Leverage dynamic, containerized environments for true isolation. • Docker Compose or Testcontainers can spin up a fresh, dedicated microservice stack (including databases) for each Playwright worker process or even per test file. • Use globalSetup and globalTeardown in playwright.config.ts to orchestrate container lifecycle. globalSetup starts the environments, globalTeardown cleans them up. • Each environment gets unique port mappings and database instances, preventing any cross-talk.

• State Provisioning: API-First, Idempotent Fixtures Use custom Playwright fixtures to provision specific states before UI interaction via direct API or database calls. This is significantly faster than UI-driven setup. • Ensure provisioning logic is idempotent: running it multiple times yields the same state without errors.

// playwright/fixtures/statefulUser.ts
import { test as base } from '@playwright/test';
import axios from 'axios'; // Example for API interaction

type MyFixtures = {
  statefulUser: { userId: string; token: string; orderId: string };
};

export const test = base.extend<MyFixtures>({
  statefulUser: [async ({}, use) => {
    // 1. Create unique user via API
    const userRes = await axios.post('http://localhost:8080/api/users', { name: 'Test User' });
    const userId = userRes.data.id;
    const token = userRes.data.token;

    // 2. Provision complex state (e.g., 3 orders) via API
    await axios.post(`http://localhost:8080/api/users/${userId}/orders`, { items: ['itemA'] });
    await axios.post(`http://localhost:8080/api/users/${userId}/orders`, { items: ['itemB'] });
    const orderRes = await axios.post(`http://localhost:8080/api/users/${userId}/orders`, { items: ['itemC'] });
    const orderId = orderRes.data.id;

    // Provide the stateful data to the test
    await use({ userId, token, orderId });

    // Optional: Teardown specific user state if not handled by ephemeral env cleanup
    // await axios.delete(`http://localhost:8080/api/users/${userId}`);
  }, { scope: 'test' }], // 'test' scope for per-test fixture
});

// Example test usage:
// import { test } from '../fixtures/statefulUser';
// test('should display user orders', async ({ page, statefulUser }) => {
//   await page.goto(`/dashboard?user=${statefulUser.userId}`);
//   // Assert on page content using statefulUser.orderId etc.
// });

• Resource Cleanup: Robust Teardown • For ephemeral environments, globalTeardown is critical to gracefully stop and remove containers. • Within tests, test.afterEach can handle specific cleanup (e.g., logging out, deleting temporary files). • Crucially, ensure cleanup hooks execute even if a test fails to prevent resource leaks that impact subsequent runs. Playwright’s afterEach and globalTeardown handle this by default.

🔑 Key Takeaways • Isolation First: Use container orchestration (Docker Compose, Testcontainers) for truly isolated, ephemeral backend environments per worker. • API-Driven State: Provision complex test prerequisites via direct API/DB calls using custom Playwright fixtures for speed and idempotency. • Robust Cleanup: Implement globalTeardown for environment destruction and afterEach for test-specific cleanup, ensuring execution even on failure.

❓ Quick Summary Q&A • Q: How to ensure isolation? A: Ephemeral containerized environments (Docker) per worker, orchestrated by Playwright’s globalSetup/globalTeardown. • Q: How to provision state fast? A: Custom Playwright fixtures making direct, idempotent API/DB calls before UI interaction. • Q: How to clean up reliably? A: globalTeardown for environments, afterEach for test artifacts, designed to run even upon test failure.


📲 Practice Offline on Mobile: Download the free QA Automation & SDET Prep app on Google Play & App Store.