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.