How do you manage shared, stateful test data efficiently when running multiple Playwright tests in parallel workers?
Question
How do you manage shared, stateful test data efficiently when running multiple Playwright tests in parallel workers?
Answer
When running tests in parallel using Playwright’s worker setup (e.g., through test runners like Jest or Playwright Test Runner), the fundamental principle is that each worker process should be highly isolated. Shared, mutable state across these workers is an anti-pattern that guarantees flakiness and race conditions.
The goal of a medium-level solution is not to find a way to share mutable in-memory state (which is impossible or highly dangerous), but rather to manage the access to necessary state efficiently and safely.
Here is the production-grade approach, prioritized by isolation and scalability:
1. The Ideal Solution: Immutable Data via Fixtures (Isolation)
For data that is read-only or required for setup (e.g., a list of user IDs, a configuration object), the best practice is to load this data once and inject it via fixtures.
In Playwright Test, you define fixtures that are scoped to the test suite or project. This ensures the data is loaded, but the test execution remains isolated.
// conftest.ts or test.beforeAll setup
import { test as base } from '@playwright/test';
// Define a shared, read-only data source
const sharedTestData = {
userEmails: ['[email protected]', '[email protected]'],
apiEndpoints: {
login: '/api/login',
user: '/api/users'
}
};
// Extend the base test object with a fixture
export const test = base.extend<{ sharedData: typeof sharedTestData }>({
sharedData: [sharedTestData, { scope: 'project' }], // Scope ensures it's loaded once per project run
});
// Usage in a test file
test('should validate multiple users using shared data', async ({ sharedData }) => {
for (const email of sharedData.userEmails) {
// Test logic using the immutable data
await page.goto(`/login?email=${email}`);
// ... assertions
}
});
2. The Stateful Solution: External Service Interaction (Mutability)
If the data must be mutable (e.g., Test A creates a user, and Test B needs to log in as that specific user), the tests should not share memory. They must share an external, controlled service.
Pattern: Test Setup $\rightarrow$ External API $\rightarrow$ Test Execution $\rightarrow$ External API Cleanup.
- Setup (Test A): Test A uses a dedicated API client (outside of Playwright UI actions) to create the required resource (e.g.,
POST /api/users). It captures the unique ID (user_id: 123). - Injection: The unique ID is stored in a configuration object or an environment variable accessible by all related tests.
- Execution (Test B): Test B reads the
user_idfrom the configuration and uses it to perform UI actions (e.g.,GET /api/users/123). - Teardown: Test A or a dedicated cleanup routine deletes the resource (
DELETE /api/users/123).
This decouples the state from the worker process, relying on the database/API layer as the single source of truth.
Summary Table
| Data Type | Mutability | Storage Method | Playwright Mechanism | Scalability/Safety |
|---|---|---|---|---|
| Configuration/List | Immutable (Read-only) | Configuration File/Fixture | Fixtures (scope: 'project') | High. Workers receive a copy of the data. |
| Resource State | Mutable (Write/Read) | External Database/API | API Client (outside of UI) + Env Vars | High. State is managed by the persistence layer, not the test runner. |
| In-Memory Global | Mutable | (N/A) | Global Variables/Shared Objects | Low/Dangerous. Avoid completely. |