Test Data Management

38Test Data Management (TDM)

What you will master here

  • Why TDM matters and what bad TDM looks like
  • Strategies: refresh, subset, synthetic, masked
  • Data factories & builders in code
  • PII masking / anonymisation rules
  • Per-tenant / per-test isolation patterns
  • Seeding via API / SQL / fixture files
  • Cleanup, leases, and "noisy neighbour" prevention

38.1 Bad TDM looks like…

38.2 The four classical TDM strategies

StrategyWhat it isProsCons
Production cloneCopy of prod DBRealistic dataPII exposure, expensive to refresh, hard to mutate
Production subset + maskSelected rows, sensitive fields maskedRealistic shape, saferMasking pipeline to maintain
Synthetic generationFaker-style fake but realistic-looking dataSafe, on-demand, infiniteMay miss real-world weirdness
HybridSynthetic for most, masked-prod for hard casesBest of bothMost plumbing

38.3 Per-test factories (code-driven TDM)

// src/factories/userFactory.ts
import { faker } from '@faker-js/faker';
import type { APIRequestContext } from '@playwright/test';

export interface User { id: string; email: string; name: string; role: 'admin' | 'user'; }

export function userPayload(over: Partial<User> = {}) {
  return {
    email: faker.internet.email({ provider: 'qa.acme.test' }),  // never @gmail.com
    name: faker.person.fullName(),
    role: 'user' as const,
    ...over,
  };
}

export async function createUser(api: APIRequestContext, over: Partial<User> = {}): Promise<User> {
  const res = await api.post('/test/users', { data: userPayload(over) });
  if (!res.ok()) throw new Error(`createUser failed: ${res.status()}`);
  return res.json();
}

export async function deleteUser(api: APIRequestContext, id: string) {
  await api.delete(`/test/users/${id}`);
}

Use in a fixture:

export const test = base.extend<{ user: User }>({
  user: async ({ request }, use) => {
    const u = await createUser(request);
    await use(u);
    await deleteUser(request, u.id);
  },
});

38.4 Builder pattern for complex objects

class OrderBuilder {
  private order: Partial<Order> = { items: [], status: 'pending' };
  withUser(u: User)          { this.order.userId = u.id; return this; }
  withItem(sku: string, qty=1){ this.order.items!.push({ sku, qty }); return this; }
  withCoupon(code: string)   { this.order.coupon = code; return this; }
  withStatus(s: OrderStatus) { this.order.status = s; return this; }
  async build(api: APIRequestContext): Promise<Order> {
    return (await api.post('/test/orders', { data: this.order })).json();
  }
}

const order = await new OrderBuilder()
  .withUser(user).withItem('SHOE-42', 2).withCoupon('TEST50').build(request);

38.5 Per-tenant isolation

Each parallel test slot gets its own tenant (a top-level data namespace). Tests within a slot share the tenant safely; tests across slots can't collide.

// fixtures.ts
export const test = base.extend<{}, { tenant: string }>({
  tenant: [async ({}, use, info) => {
    const t = `qa-slot-${info.parallelIndex}`;
    await use(t);
  }, { scope: 'worker' }],
});

The factory / API call includes tenant in headers; the backend scopes data accordingly.

38.6 PII masking — non-negotiable for prod-derived data

FieldMask example
Emailuser-7a3f@masked.test
NameFaker fake name with consistent locale
PhoneFormat kept (digits → 9), area code retained
Credit card4242424242424242 (Stripe test PAN) or all 9s
SSN / AadhaarRandom with matching format + checksum
AddressRandom street, real city retained
DOBShift by random days while preserving age bucket

Tools: Tonic.ai, Delphix, GDPR-mask, custom Python pipeline. Whatever you pick: keep the mapping deterministic within a refresh (the same prod email always maps to the same test email) — otherwise referential integrity breaks.

38.7 Cleanup & leases

38.8 Time-sensitive data

38.9 Common SDET TDM mistakes

Module 31 — TDM Q&A bank

What's the goal of test data management?
Provide every test with the data it needs — correct shape, isolated from other tests, realistic enough to exercise the system — without manual intervention. Bad TDM is the most common silent killer of test reliability.
Should you use production data for testing?
Only if every PII field is masked deterministically and consistently across tables. Even then, it's regulated (GDPR, CCPA) and expensive to refresh. Default to synthetic data; reach for prod-derived only when realism matters and your security/legal sign-off allows it.
What's a data factory?
A function (or builder) that returns a valid default object with optional overrides. userFactory({ role: 'admin' }) creates a user with default fields and admin role. Keeps tests focused on intent ("a user", "an admin") not data plumbing.
When is the Builder pattern better than a factory function?
When the object is complex with many optional fields and the construction order matters. new OrderBuilder().withUser(u).withItem(sku).withCoupon(c).build() reads better than a 12-property object literal.
How do you handle data isolation in parallel tests?
Either per-test creation+cleanup, or per-slot tenant (each parallelIndex owns one tenant). The latter is faster when data is expensive to create; the former is simpler when creation is cheap.
What's a lease in TDM?
A time-bound reservation of a shared data resource. Slot X leases user Y for 1 hour; if the lease isn't renewed (crash), the user becomes available again. Prevents dead-lock if a test process disappears.
How do you mask production data safely?
Apply deterministic, format-preserving masking: the same email always maps to the same masked email; numbers stay numeric; addresses stay valid-looking. Tools: Tonic.ai, Delphix, custom pipelines. Mask before the data leaves prod's security boundary.
How do you test time-dependent behaviour (e.g. expires in 30 days)?
Backend test-only endpoint to set/advance the clock. Plus Playwright's page.clock API for browser-side time. Don't use real waiting; tests can't run for 30 days.
What's the danger of relying on a shared "QA user account"?
Cross-test contamination — one test changes the user's state, another fails because of it. Race conditions in parallel runs. Hard-to-reproduce flakiness. Always prefer ephemeral per-test users.
How do you ensure test data doesn't pile up over time?
Per-test cleanup in fixture teardown. Nightly cleanup job deletes anything older than N days tagged with the QA marker. Monitoring on test DB size — alert if it grows.
How does TDM relate to test stability?
Most "flaky" tests are actually data-flaky: data left in an unexpected state by a previous test, or shared with a parallel test. Strict per-test isolation removes a huge class of intermittent failures.