Skip to content

Testing Patterns: Serial vs Parallel ​

This guide explains when and how to use serial vs parallel test execution patterns.

Overview ​

PatternExecutionBrowser ContextUse Case
Parallel (default)Tests run concurrentlyEach test gets fresh contextIndependent tests
SerialTests run sequentiallyShared browser contextWorkflow tests

Parallel Tests (Default) ​

By default, Playwright runs tests in parallel with isolated browser contexts.

Characteristics ​

  • Isolation: Each test gets a fresh browser context
  • Speed: Tests run concurrently across workers
  • Independence: Tests cannot affect each other
  • Login overhead: Each test must login separately

When to Use ​

  • Tests that don't depend on each other
  • Tests that modify shared state (to avoid conflicts)
  • General functional tests
  • Smoke tests

Example ​

typescript
import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";

// Each test runs independently with its own browser context
test.describe("Catalog Tests", () => {
  test.beforeEach(async ({ loginHelper }) => {
    // Each test logs in fresh
    await loginHelper.loginAsGuest();
  });

  test("can view catalog", async ({ uiHelper }) => {
    await uiHelper.openSidebar("Catalog");
    await uiHelper.verifyHeading("My Org Catalog");
  });

  test("can search catalog", async ({ uiHelper }) => {
    await uiHelper.openSidebar("Catalog");
    await uiHelper.searchInputPlaceholder("Filter", "component");
  });

  test("can filter by kind", async ({ uiHelper }) => {
    await uiHelper.openSidebar("Catalog");
    await uiHelper.selectMuiBox("Kind", "API");
  });
});

Configuration ​

typescript
// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { baseConfig } from "@red-hat-developer-hub/e2e-test-utils/playwright-config";

export default defineConfig({
  ...baseConfig,
  workers: 4, // Run 4 tests in parallel
  fullyParallel: true, // All tests in parallel
});

Serial Tests (Shared Session) ​

Serial tests share a browser context and run in sequence.

Characteristics ​

  • Shared state: Tests share the same browser session
  • Order dependency: Tests run in defined order
  • Efficiency: Single login for all tests
  • Workflow testing: Perfect for multi-step flows

When to Use ​

  • Testing multi-step workflows
  • Tests that build on each other
  • Resource-intensive operations (single login)
  • State-dependent test sequences

Example ​

typescript
import { test } from "@playwright/test";
import { setupBrowser, UIhelper, LoginHelper } from "@red-hat-developer-hub/e2e-test-utils/helpers";

// Configure as serial
test.describe.configure({ mode: "serial" });

test.describe("Entity Creation Workflow", () => {
  let uiHelper: UIhelper;
  let loginHelper: LoginHelper;

  test.beforeAll(async ({ browser }) => {
    // Setup shared browser context
    const context = await setupBrowser(browser);
    const page = await context.newPage();
    uiHelper = new UIhelper(page);
    loginHelper = new LoginHelper(page, uiHelper);

    // Single login for all tests
    await loginHelper.loginAsGuest();
  });

  test("step 1: navigate to create", async () => {
    await uiHelper.openSidebar("Create...");
    await uiHelper.verifyHeading("Create");
  });

  test("step 2: fill template form", async () => {
    // Uses same session from step 1
    await uiHelper.clickButton("Choose");
    await uiHelper.fillTextInputByLabel("Name", "my-new-component");
    await uiHelper.fillTextInputByLabel("Description", "A test component");
  });

  test("step 3: submit and verify", async () => {
    // Uses same session from steps 1-2
    await uiHelper.clickButton("Create");
    await uiHelper.verifyHeading("my-new-component");
  });

  test("step 4: verify in catalog", async () => {
    // Uses same session from previous steps
    await uiHelper.openSidebar("Catalog");
    await uiHelper.verifyRowsInTable(["my-new-component"]);
  });
});

Key Differences from Parallel ​

AspectParallelSerial
test.describeNormaltest.describe.configure({ mode: "serial" })
FixturesUse { uiHelper }Create manually with new UIhelper(page)
Browser setupAutomaticUse setupBrowser(browser)
LoginEach beforeEachOnce in beforeAll
State between testsIsolatedShared

Hybrid Approach ​

You can mix parallel and serial within the same test file:

typescript
import { test, expect } from "@red-hat-developer-hub/e2e-test-utils/test";
import { setupBrowser, UIhelper, LoginHelper } from "@red-hat-developer-hub/e2e-test-utils/helpers";

// Parallel tests (use fixtures)
test.describe("Parallel Catalog Tests", () => {
  test.beforeEach(async ({ loginHelper }) => {
    await loginHelper.loginAsGuest();
  });

  test("view catalog", async ({ uiHelper }) => {
    await uiHelper.openSidebar("Catalog");
  });

  test("view APIs", async ({ uiHelper }) => {
    await uiHelper.openSidebar("APIs");
  });
});

// Serial tests (shared session)
test.describe("Serial Workflow Tests", () => {
  test.describe.configure({ mode: "serial" });

  let uiHelper: UIhelper;
  let loginHelper: LoginHelper;

  test.beforeAll(async ({ browser }) => {
    const context = await setupBrowser(browser);
    const page = await context.newPage();
    uiHelper = new UIhelper(page);
    loginHelper = new LoginHelper(page, uiHelper);
    await loginHelper.loginAsGuest();
  });

  test("step 1", async () => { /* ... */ });
  test("step 2", async () => { /* ... */ });
});

Best Practices ​

For Parallel Tests ​

  1. Keep tests independent - Each test should work in isolation
  2. Use unique data - Generate unique names to avoid conflicts
  3. Clean up after - Remove any created resources
  4. Don't rely on order - Tests may run in any sequence
typescript
test("create entity with unique name", async ({ apiHelper }) => {
  const uniqueName = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`;

  await apiHelper.importEntity(`https://example.com/${uniqueName}/catalog-info.yaml`);

  // Clean up
  test.afterEach(async () => {
    await apiHelper.deleteEntity(uniqueName);
  });
});

For Serial Tests ​

  1. Use meaningful step names - Makes failures easier to diagnose
  2. Handle cleanup properly - Clean up in afterAll, not afterEach
  3. Consider failure impact - Later tests may fail if early ones fail
  4. Minimize shared state - Only share what's necessary
typescript
test.describe("Workflow with proper cleanup", () => {
  test.describe.configure({ mode: "serial" });

  let createdEntity: string;

  test.afterAll(async ({ browser }) => {
    // Clean up after all tests complete (or fail)
    if (createdEntity) {
      // Cleanup logic here
    }
  });

  test("create entity", async () => {
    createdEntity = "my-entity";
    // ... creation logic
  });
});

Performance Comparison ​

ScenarioParallel (4 workers)Serial
10 independent tests~30 seconds~2 minutes
5-step workflowN/A (can't parallelize)~1 minute
10 tests with shared login~30 sec + 10 logins~1 min + 1 login

Decision Flowchart ​

┌─────────────────────────────────────────┐
│ Do tests depend on each other's state?  │
└─────────────────────┬───────────────────┘
                      │
         ┌────────────┴────────────┐
         │                         │
        Yes                        No
         │                         │
         ▼                         ▼
┌─────────────────┐    ┌─────────────────────┐
│  Use Serial     │    │ Is login expensive? │
│  mode: "serial" │    └──────────┬──────────┘
└─────────────────┘               │
                      ┌───────────┴───────────┐
                      │                       │
                     Yes                      No
                      │                       │
                      ▼                       ▼
             ┌────────────────┐    ┌─────────────────┐
             │Consider Serial │    │  Use Parallel   │
             │for same user   │    │  (default)      │
             └────────────────┘    └─────────────────┘

Released under the Apache-2.0 License.