Playwright·Cookbook Field Manual
Part 04 · Architecture, Auth & Data Card 20

API Seeding & Cleanup

Card 20: API Seeding and Cleanup

What This Pattern Solves

Tests that create data (entities, records, API resources) need to clean up after themselves, otherwise subsequent runs fail from collisions - duplicate IDs, stale state, or capacity limits. Simple afterEach hooks are unreliable because they run even when setup fails (no IDs to clean) and can’t be composed for complex flows. This card introduces factory functions for creating test entities, try/finally for guaranteed cleanup, and unique naming so parallel test workers never clash.

How It Works

  1. Define a factory function (createClient(partial, id)) that takes an explicit id from the caller and stores the entity. The id comes from the worker-namespaced unique(testInfo, ...) helper, so it’s collision-free and reproducible — no unseeded Math.random().
  2. Track created IDs in an array so you know exactly what to tear down
  3. Use try { ... } finally { cleanup(ids) } instead of afterEach — cleanup runs deterministically, even if the test fails
  4. Give each entity a unique name via unique(testInfo, 'prefix') so parallel workers can’t collide
  5. Assert cleanup worked: after finally runs, the store should be empty or the created IDs invalid
  6. For repeated patterns, promote the factory + cleanup pair into a fixture (see Card 07)

Code Example

import { test as base, expect } from '@playwright/test';
import { unique } from '../e2e-patterns/helpers/unique';

type Client = { id: string; name: string };
const store = new Map<string, Client>();

// The id is supplied by the caller using the worker-namespaced `unique()`
// helper, so seeded data is collision-free under parallel runs and the id is
// reproducible from the test (no unseeded Math.random()).
function createClient(partial: Partial<Client>, id: string): Client {
  const client = { id, name: partial.name ?? 'Unnamed', ...partial };
  store.set(id, client);
  return client;
}

async function cleanup(ids: string[]): Promise<void> {
  await Promise.all(
    ids.map((id) => {
      store.delete(id);
      return Promise.resolve();
    }),
  );
}

let workerStoreCleanupCount = 0;

const test = base.extend<object, { workerCleanupTracker: number }>({
  workerCleanupTracker: [
    async ({}, use) => {
      await use(++workerStoreCleanupCount);
      store.clear();
    },
    { scope: 'worker' },
  ],
});

test.describe('20-api-seeding-cleanup: Factories and cleanup', () => {
  test.beforeEach(() => {
    store.clear();
  });

  test('createClient + cleanup in finally', async ({}, testInfo) => {
    const createdIds: string[] = [];
    try {
      const client = createClient(
        { name: unique(testInfo, 'client') },
        unique(testInfo, 'client-id'),
      );
      createdIds.push(client.id);
      expect(store.has(client.id)).toBe(true);
      expect(client.name).toContain('client');
    } finally {
      await cleanup(createdIds);
    }
    expect(store.size).toBe(0);
  });

  test('cleanup in finally runs and removes created ids', async (
    {},
    testInfo,
  ) => {
    const createdIds: string[] = [];
    try {
      const client = createClient(
        { name: unique(testInfo, 'client') },
        unique(testInfo, 'client-id'),
      );
      createdIds.push(client.id);
    } finally {
      await cleanup(createdIds);
    }
    expect(createdIds.every((id) => !store.has(id))).toBe(true);
  });

  test('request.post factory: seed via API with auth headers', async ({
    request,
  }) => {
    const response = await request.get('/api/health', {
      headers: {
        Authorization: 'Bearer test-token',
      },
    });

    expect(response.status()).toBe(200);
  });
});

Run This Example

pnpm test src/20-api-seeding-cleanup

Prerequisites

Key Concepts

When to Use This Pattern

Common Mistakes

  1. Using afterEach instead of try/finally:

    // ❌ WRONG — afterEach runs even when beforeEach fails (nothing to clean)
    test.afterEach(async () => { await cleanup(allIds); });
    
    // ✓ CORRECT — finally runs only after the try block, with access to local scope
    const ids: string[] = [];
    try {
      const client = createClient({}, unique(testInfo, 'client-id'));
      ids.push(client.id);
    } finally {
      await cleanup(ids);
    }
  2. Not tracking created IDs explicitly:

    // ❌ WRONG — deleteEverything() is too broad, wipes other test data
    await deleteAllRecords();
    
    // ✓ CORRECT — track what you created, delete only those
    const ids: string[] = [];
    ids.push(createClient({}, unique(testInfo, 'client-id')).id);
    // ... later
    await cleanup(ids);
  3. Hardcoded names causing collisions in parallel runs:

    // ❌ WRONG — two parallel workers both create "test-client" with a shared id
    createClient({ name: 'test-client' }, 'test-client');
    
    // ✓ CORRECT — unique name AND unique id from testInfo metadata
    createClient(
      { name: unique(testInfo, 'client') },
      unique(testInfo, 'client-id'),
    );
  4. Skipping the post-cleanup assertion:

    // ❌ WRONG — cleanup ran, but did it actually delete?
    } finally { await cleanup(ids); }
    
    // ✓ CORRECT — assert the store is empty after cleanup
    } finally { await cleanup(ids); }
    expect(store.size).toBe(0);

Flow Diagram

sequenceDiagram
    participant Test
    participant Factory
    participant Store
    participant Cleanup

    Test->>Factory: createClient({ name: unique(...) }, unique(testInfo, 'client-id'))
    Note over Factory: id supplied by caller (no Math.random)
    Factory->>Store: set(id, client)
    Factory-->>Test: Client { id, name }

    Test->>Store: expect(store.has(id)).toBe(true)
    Note over Test: Assertions pass/fail...

    Test->>Cleanup: finally { cleanup(ids) }
    Cleanup->>Store: delete each id in ids[]
    Store-->>Cleanup: All ids removed

    Test->>Store: expect(store.size).toBe(0)
    Note over Test: Verify cleanup worked

Live Demo

👇 This component is what the Playwright test interacts with:

Loading…

Run This Example

pnpm test src/20-api-seeding-cleanup