← Blog

"Playwright Series #9: Parallel Execution, Sharding and Reporting"

A 10-minute test suite becomes a 2-minute test suite with the right parallelism strategy. Learn workers, sharding across machines, flaky test retry, and reports your team will actually read.

reading now
views
comments

Series Navigation

Part 8: Visual Testing — Screenshots and Pixel-Perfect Comparisons

Part 10: CI/CD Integration — GitLab, GitHub Actions and Docker


How Playwright Runs Tests

By default Playwright runs tests in parallel across multiple workers. Each worker is an independent process with its own browser instance. Workers run different test files simultaneously.

Worker 1 → login.spec.ts        ─┐
Worker 2 → checkout.spec.ts      ├─ all running simultaneously
Worker 3 → dashboard.spec.ts     │
Worker 4 → admin.spec.ts        ─┘

Within a single file, tests run sequentially by default unless you opt in to parallel within a file.


Configuring Workers

// playwright.config.ts
export default defineConfig({
  // Use all available CPU cores
  workers: undefined,         // default — Playwright decides

  // Use exactly 4 workers
  workers: 4,

  // Use 50% of available CPU cores
  workers: '50%',

  // In CI: use fewer workers to avoid resource contention
  workers: process.env.CI ? 2 : undefined,
});

Too many workers in CI can cause instability if resources are constrained. Start with 2–4 in CI and tune from there.


Parallel Tests Within a File

By default tests within a file run one after another. Enable parallel execution within a file:

import { test } from '@playwright/test';

// All tests in this file run in parallel
test.describe.configure({ mode: 'parallel' });

test('test A', async ({ page }) => { ... });
test('test B', async ({ page }) => { ... });
test('test C', async ({ page }) => { ... });

Be careful: tests in the same file that share state (e.g. same database records) can conflict when run in parallel.


Test Isolation — The Rule for Safe Parallelism

The golden rule: each test must be fully independent. A test should:

  • Create its own data
  • Not depend on output from another test
  • Clean up after itself (or use isolated data like unique IDs)
// ❌ Tests depend on each other — breaks in parallel
test('creates a product', async ({ page }) => {
  await page.goto('/admin/products/new');
  await page.getByLabel('Name').fill('Widget');
  await page.getByRole('button', { name: 'Save' }).click();
});

test('deletes the product', async ({ page }) => {
  // Fails if "creates a product" didn't run first
  await page.goto('/admin/products');
  await page.getByRole('row', { name: 'Widget' }).getByRole('button', { name: 'Delete' }).click();
});

// ✅ Each test is self-contained
test('can delete a product', async ({ page, request }) => {
  // Create the product via API (fast, isolated)
  const response = await request.post('/api/products', {
    data: { name: `Widget-${Date.now()}` } // unique name
  });
  const { id } = await response.json();

  // Delete it via UI
  await page.goto('/admin/products');
  await page.getByRole('row', { name: new RegExp(`Widget-`) }).getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByRole('alert')).toContainText('Product deleted');
});

Sharding — Splitting Across Multiple Machines

For very large test suites (500+ tests), sharding splits the suite across multiple CI machines running simultaneously:

# Machine 1: run shard 1 of 4
npx playwright test --shard=1/4

# Machine 2: run shard 2 of 4
npx playwright test --shard=2/4

# Machine 3: run shard 3 of 4
npx playwright test --shard=3/4

# Machine 4: run shard 4 of 4
npx playwright test --shard=4/4

If your full suite takes 20 minutes on one machine, 4 shards bring it to ~5 minutes wall-clock time.

Each shard produces its own report blob. Merge them after all shards complete:

npx playwright merge-reports --reporter html ./blob-reports/
npx playwright show-report

Retrying Flaky Tests

Configure automatic retries for tests that fail intermittently:

// playwright.config.ts
export default defineConfig({
  // Retry failed tests 2 times in CI, 0 times locally
  retries: process.env.CI ? 2 : 0,
});

Retry a specific test more aggressively:

test('payment webhook (external service)', async ({ page }) => {
  test.info().annotations.push({ type: 'flaky', description: 'External webhook timing' });
  // ...
}, { retries: 3 });

Important: retries hide flakiness, they don't fix it. Use retries as a short-term safety net while you investigate the root cause.


Reporters

List Reporter (development)

Running 24 tests using 4 workers

  ✓  login › redirects to dashboard (834ms)
  ✓  shop › adds item to cart (612ms)
  ✗  checkout › payment fails with expired card (1.2s)
  ✓  admin › creates new user (945ms)

HTML Reporter (teams)

reporter: [['html', { open: 'never', outputFolder: 'playwright-report' }]]

Generates a full interactive report with:

  • Test results tree
  • Failure screenshots
  • Video recordings
  • Network request log
  • Trace timeline

Open it locally:

npx playwright show-report

JUnit Reporter (CI integration)

reporter: [
  ['list'],                              // terminal output
  ['junit', { outputFile: 'results.xml' }], // for CI test dashboards
  ['html', { open: 'never' }],           // for team review
]

Most CI systems (GitLab, GitHub Actions, Jenkins) can parse JUnit XML and display results natively in the pipeline UI.

Multiple Reporters

reporter: process.env.CI
  ? [['dot'], ['junit', { outputFile: 'results.xml' }], ['blob', { outputDir: './blob-reports' }]]
  : [['list'], ['html', { open: 'on-failure' }]];

blob reporter saves reports as blobs for later merging (useful with sharding).


Trace Viewer — Debugging Failures

Playwright Trace Viewer records a full timeline of every test action — DOM snapshots, network requests, console logs, screenshots at each step.

Enable in config:

use: {
  trace: 'on-first-retry',      // record on first retry (recommended)
  // trace: 'retain-on-failure', // keep traces for all failed tests
  // trace: 'on',                // record everything (expensive)
}

Open a trace:

npx playwright show-trace test-results/checkout-payment/trace.zip

Or open from the HTML report — click any failed test, then "Trace".

The trace lets you:

  • Step through every action
  • See the DOM state at each point
  • Inspect network requests and responses
  • See console errors at the exact moment they occurred
  • Watch a video of the test execution

This is the single most powerful debugging tool in Playwright.


Test Annotations and Tags

Annotate tests for filtering and reporting:

test('processes refunds @smoke @payments', async ({ page }) => {
  // ...
});

// Run only smoke tests
// npx playwright test --grep @smoke

// Skip payment tests in this environment
test('payment flow', async ({ page }) => {
  test.skip(process.env.SKIP_PAYMENTS === 'true', 'Payment tests disabled');
  // ...
});

// Mark known issues
test('dropdown position bug', async ({ page }) => {
  test.fail(true, 'Known bug — tracked in JIRA-1234');
  // test runs, but is expected to fail
});

Measuring Test Performance

# Output timing per test
npx playwright test --reporter=list

# Find the 10 slowest tests
npx playwright test --reporter=json | jq '[.suites[].specs[].tests[] | {title: .title, duration: .results[0].duration}] | sort_by(-.duration) | .[0:10]'

Identify and optimise slow tests:

  • Tests slow due to UI login → use storageState (Part 7)
  • Tests slow due to real API calls → mock the network (Part 6)
  • Tests slow due to waitForTimeout → replace with proper assertions

What's Next

In Part 10 — the final post in this series — we put it all together with CI/CD integration: GitLab CI pipelines, GitHub Actions workflows, Docker containers for consistent environments, and a production-ready pipeline that runs your entire Playwright suite on every pull request.

Discussion

Loading...

Leave a Comment

All comments are reviewed before appearing. No links please.

0 / 1000