Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
infrastructure-ci-cd/parallel-sharding.md
1# Sharding and Parallel Execution23## Table of Contents451. [CLI Commands](#cli-commands)62. [Patterns](#patterns)73. [Decision Guide](#decision-guide)84. [Anti-Patterns](#anti-patterns)95. [Troubleshooting](#troubleshooting)1011> **When to use**: Speeding up test suites by running tests concurrently on one machine (workers) or splitting across multiple CI jobs (sharding).1213## CLI Commands1415```bash16# Parallelism within one machine17npx playwright test --workers=418npx playwright test --workers=50%1920# Splitting across CI jobs21npx playwright test --shard=1/422npx playwright test --shard=2/42324# Merging shard outputs25npx playwright merge-reports ./blob-report26npx playwright merge-reports --reporter=html,json ./blob-report2728# Override config for single run29npx playwright test --fully-parallel30```3132## Patterns3334### Worker Configuration3536**Use when**: Controlling concurrent test execution on a single machine.3738```ts39// playwright.config.ts40import { defineConfig } from "@playwright/test";4142export default defineConfig({43// Tests WITHIN a file also run in parallel44fullyParallel: true,4546// Worker count options:47// - undefined: auto-detect (half CPU cores)48// - number: fixed count49// - string: percentage of cores50workers: process.env.CI ? "50%" : undefined,51});52```5354**`fullyParallel` behavior:**5556| Setting | Files parallel | Tests in file parallel |57| -------------------------------- | -------------- | ---------------------- |58| `fullyParallel: false` (default) | Yes | No (serial) |59| `fullyParallel: true` | Yes | Yes |6061**Serial execution for specific files:**6263```ts64// tests/checkout-flow.spec.ts65import { test, expect } from "@playwright/test";6667test.describe.configure({ mode: "serial" });6869test("add items to cart", async ({ page }) => {70// ...71});7273test("complete payment", async ({ page }) => {74// ...75});76```7778### Sharding Across CI Machines7980**Use when**: Suite exceeds 5 minutes even with maximum workers.8182```bash83# Job 1 Job 2 Job 3 Job 484--shard=1/4 --shard=2/4 --shard=3/4 --shard=4/485```8687**Config for sharded runs:**8889```ts90// playwright.config.ts91import { defineConfig } from "@playwright/test";9293export default defineConfig({94fullyParallel: true,95workers: process.env.CI ? "50%" : undefined,9697reporter: process.env.CI98? [["blob"], ["github"]]99: [["html", { open: "on-failure" }]],100});101```102103### Merging Shard Reports104105**Use when**: Combining blob reports from multiple shards into a unified report.106107```bash108# Merge all blobs into HTML109npx playwright merge-reports --reporter=html ./all-blob-reports110111# Multiple formats112npx playwright merge-reports --reporter=html,json,junit ./all-blob-reports113114# Custom output location115PLAYWRIGHT_HTML_REPORT=merged-report npx playwright merge-reports --reporter=html ./all-blob-reports116```117118**GitHub Actions merge job:**119120```yaml121merge-reports:122if: ${{ !cancelled() }}123needs: test124runs-on: ubuntu-latest125steps:126- uses: actions/checkout@v4127- run: npm ci128129- uses: actions/download-artifact@v4130with:131path: all-blob-reports132pattern: blob-report-*133merge-multiple: true134135- run: npx playwright merge-reports --reporter=html ./all-blob-reports136137- uses: actions/upload-artifact@v4138with:139name: playwright-report140path: playwright-report/141retention-days: 14142```143144### Worker-Scoped Fixtures145146**Use when**: Expensive resources (DB connections, auth tokens) should be created once per worker, not per test.147148```ts149// fixtures.ts150import { test as base } from "@playwright/test";151152type WorkerFixtures = {153dbClient: DatabaseClient;154apiToken: string;155};156157export const test = base.extend<{}, WorkerFixtures>({158dbClient: [159async ({}, use) => {160const client = await DatabaseClient.connect(process.env.DB_URL!);161await use(client);162await client.disconnect();163},164{ scope: "worker" },165],166167apiToken: [168async ({}, use, workerInfo) => {169const res = await fetch(`${process.env.API_URL}/auth`, {170method: "POST",171headers: { "Content-Type": "application/json" },172body: JSON.stringify({173user: `test-user-${workerInfo.workerIndex}`,174password: process.env.TEST_PASSWORD,175}),176});177const { token } = await res.json();178await use(token);179},180{ scope: "worker" },181],182});183184export { expect } from "@playwright/test";185```186187### Test Isolation for Parallelism188189**Use when**: Preparing tests to run without interference.190191Each test must create its own state. No test should depend on or modify shared state.192193```ts194// BAD: Shared user causes race conditions195test("edit settings", async ({ page }) => {196await page.goto("/users/test-user/settings");197await page.getByLabel("Email").fill("[email protected]");198await page.getByRole("button", { name: "Save" }).click();199});200201// GOOD: Unique user per test202test("edit settings", async ({ page, request }) => {203const res = await request.post("/api/users", {204data: { name: `user-${Date.now()}`, email: `${Date.now()}@test.com` },205});206const user = await res.json();207208await page.goto(`/users/${user.id}/settings`);209await page.getByLabel("Email").fill("[email protected]");210await page.getByRole("button", { name: "Save" }).click();211await expect(page.getByLabel("Email")).toHaveValue("[email protected]");212213await request.delete(`/api/users/${user.id}`);214});215```216217**Using `testInfo` for unique identifiers:**218219```ts220import { test, expect } from "@playwright/test";221222test("submit order", async ({ page }, testInfo) => {223const orderId = `order-${testInfo.workerIndex}-${Date.now()}`;224await page.goto(`/orders/new?ref=${orderId}`);225// ...226});227```228229### Dynamic Shard Count230231**Use when**: Automatically adjusting shards based on test count.232233```yaml234# .github/workflows/playwright.yml235jobs:236calculate-shards:237runs-on: ubuntu-latest238outputs:239shard-count: ${{ steps.calc.outputs.count }}240shard-matrix: ${{ steps.calc.outputs.matrix }}241steps:242- uses: actions/checkout@v4243- run: npm ci244- id: calc245run: |246TEST_COUNT=$(npx playwright test --list --reporter=json 2>/dev/null | node -e "247const data = require('fs').readFileSync('/dev/stdin', 'utf8');248const parsed = JSON.parse(data);249console.log(parsed.suites?.reduce((acc, s) => acc + (s.specs?.length || 0), 0) || 0);250")251# 1 shard per 20 tests, min 1, max 8252SHARDS=$(( (TEST_COUNT + 19) / 20 ))253SHARDS=$(( SHARDS > 8 ? 8 : SHARDS ))254SHARDS=$(( SHARDS < 1 ? 1 : SHARDS ))255MATRIX="["256for i in $(seq 1 $SHARDS); do257[ $i -gt 1 ] && MATRIX+=","258MATRIX+="\"$i/$SHARDS\""259done260MATRIX+="]"261echo "count=$SHARDS" >> $GITHUB_OUTPUT262echo "matrix=$MATRIX" >> $GITHUB_OUTPUT263264test:265needs: calculate-shards266runs-on: ubuntu-latest267strategy:268fail-fast: false269matrix:270shard: ${{ fromJson(needs.calculate-shards.outputs.shard-matrix) }}271steps:272- uses: actions/checkout@v4273- run: npm ci274- run: npx playwright install --with-deps275- run: npx playwright test --shard=${{ matrix.shard }}276```277278## Decision Guide279280| Scenario | Workers | Shards | Reason |281| -------------------------------- | -------------- | ------ | --------------------------------------- |282| < 50 tests, < 5 min | Auto (default) | None | No optimization needed |283| 50-200 tests, 5-15 min | `'50%'` in CI | 2-4 | Balance speed and cost |284| 200+ tests, > 15 min | `'50%'` in CI | 4-8 | Keep feedback under 10 min |285| Flaky due to resource contention | Reduce to 2 | Keep | Less CPU/memory pressure |286| Tests modify shared database | 1 or isolate | Useful | Sharding splits files; workers run them |287| CI has limited resources | 1 or `'25%'` | More | Compensate with more machines |288289| Aspect | Workers (in-process) | Shards (across machines) |290| -------------- | ------------------------- | -------------------------- |291| What it splits | Tests across CPU cores | Test files across CI jobs |292| Controlled by | Config or `--workers` CLI | `--shard=X/Y` CLI flag |293| Shares memory | Yes | No |294| Report merging | Not needed | Required (`merge-reports`) |295| Cost | Free (same machine) | More CI minutes |296297## Anti-Patterns298299| Anti-Pattern | Problem | Solution |300| --------------------------------------- | ---------------------------------------- | ---------------------------------------------------- |301| `fullyParallel: false` without reason | Tests in files run serially | Set `fullyParallel: true` unless tests need serial |302| `workers: 1` in CI "for safety" | Negates parallelism | Fix isolation issues; use `workers: '50%'` |303| Hardcoded shared user account | Race conditions in parallel runs | Each test creates unique data |304| Sharding without blob reporter | Each shard produces separate HTML report | Configure `reporter: [['blob']]` for CI |305| Sharding with 3 tests | Setup overhead exceeds time saved | Only shard when suite > 5 minutes |306| `test.describe.serial()` everywhere | Kills parallelism, creates dependencies | Use only when tests genuinely need prior state |307| Workers > CPU cores | Context switching overhead | Use `'50%'` or auto-detect |308| Missing `fail-fast: false` in CI matrix | One shard failure cancels others | Always set `fail-fast: false` for sharded strategies |309310## Troubleshooting311312### Tests pass solo but fail together313314- **Shared state**. Make test data unique:315```ts316test("create item", async ({ request }, ti) => {317await request.post("/api/items", {318data: { name: `Item-${ti.workerIndex}-${Date.now()}` },319});320});321```322323### "No tests found" in some shards324325- **Too many shards**. Never exceed file count:326```bash327npx playwright test --shard=1/10 # ok if 10 files328npx playwright test --shard=1/20 # too many, some shards empty329```330331### Merged report missing results332333- **Blob reports collide**. Use unique names:334```yaml335# Each shard336- uses: actions/upload-artifact@v4337with:338name: blob-report-${{ strategy.job-index }}339path: blob-report/340# Merge step341- uses: actions/download-artifact@v4342with:343pattern: blob-report-*344merge-multiple: true345path: all-blob-reports346```347348### Worker-scoped fixture not working349350- **Missing `{ scope: 'worker' }`**. Fix:351```ts352export const test = base.extend({353resource: [354async ({}, use) => {355const r = await Resource.create();356await use(r);357await r.destroy();358},359{ scope: "worker" },360],361});362```363364### More workers = Slower365366- **Too many workers thrash**. Limit in CI:367```ts368export default defineConfig({369workers: process.env.CI ? 2 : undefined,370});371```372