main*
📝e2e-testing/03-testdefaults.md
📅October 31, 20244 min read

The Foundation: TestDefaults & TestDataHelper

#e2e-testing#test-data#faker#utilities

category: "E2E Testing"

The Foundation: TestDefaults & TestDataHelper

What We're Building

Before we can build builder factories, we need a solid foundation for generating random test data and managing cleanup. This article covers:

  • TestDefaults: Random data generation (names, emails, dates, etc.)
  • TestDataHelper: Unique naming and cleanup registration

These utilities will support both direct API coverage and the seeding builders we add later.

Why We Need TestDefaults

Every test needs unique data. Hardcoding values leads to:

  • Flaky tests (duplicate data conflicts)
  • Test pollution (data leaks between tests)
  • Maintenance nightmares

TestDefaults generates random, realistic data:

// Instead of hardcoding:
const user = { name: 'John', email: 'john@test.com' }
 
// We use:
const user = {
  name: TestDefaults.firstName() + ' ' + TestDefaults.lastName(),
  email: TestDefaults.internetEmail()
};

Implementing TestDefaults

Create src/factories/TestDefaults.ts:

export class TestDefaults {
  private static firstNames = [
    'James', 'Mary', 'John', 'Patricia', 'Robert', 'Jennifer',
    'Michael', 'Linda', 'William', 'Elizabeth', 'David', 'Susan'
  ];
 
  private static lastNames = [
    'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia',
    'Miller', 'Davis', 'Rodriguez', 'Martinez', 'Hernandez', 'Lopez'
  ];
 
  private static domains = ['example.com', 'test.com', 'demo.org'];
 
  static firstName(): string {
    return this.random(this.firstNames);
  }
 
  static lastName(): string {
    return this.random(this.lastNames);
  }
 
  static fullName(): string {
    return `${this.firstName()} ${this.lastName()}`;
  }
 
  static internetEmail(firstName?: string, lastName?: string): string {
    const fn = firstName || this.firstName().toLowerCase();
    const ln = lastName || this.lastName().toLowerCase();
    const domain = this.random(this.domains);
    const num = this.numbers(3);
    return `${fn}.${ln}${num}@${domain}`;
  }
 
  static dateOfBirth(): string {
    const start = new Date(1970, 0, 1);
    const end = new Date(2000, 0, 1);
    const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
    return date.toISOString().split('T')[0];
  }
 
  static uuid(): string {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
      const r = Math.random() * 16 | 0;
      const v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  }
 
  static numbers(length: number): string {
    return Math.random().toString().slice(2, 2 + length);
  }
 
  static word(): string {
    const words = ['test', 'demo', 'sample', 'example', 'quick', 'brown', 'lazy'];
    return this.random(words);
  }
 
  static sentence(): string {
    const count = 3 + Math.floor(Math.random() * 5);
    return Array.from({ length: count }, () => this.word()).join(' ');
  }
 
  static paragraph(): string {
    const count = 3 + Math.floor(Math.random() * 3);
    return Array.from({ length: count }, () => this.sentence()).join('. ') + '.';
  }
 
  static tag(): string {
    const tags = ['react', 'javascript', 'typescript', 'node', 'python', 'java', 'go'];
    return this.random(tags);
  }
 
  private static random<T>(array: T[]): T {
    return array[Math.floor(Math.random() * array.length)];
  }
}

Implementing TestDataHelper

Create src/factories/TestDataHelper.ts:

type CleanupFn = () => Promise<void> | void;
 
export class TestDataHelper {
  private cleanupFns: Map<string, CleanupFn> = new Map();
  private createdEntities: Map<string, any[]> = new Map();
 
  constructor(private prefix: string = 'Test') {}
 
  // Generate unique names to avoid conflicts
  uniqueName(baseName?: string): string {
    const name = baseName || this.prefix;
    const timestamp = Date.now().toString(36);
    const random = Math.random().toString(36).substring(2, 6);
    return `${name}_${timestamp}_${random}`;
  }
 
  // Register cleanup for later execution
  registerCleanup(name: string, fn: CleanupFn): void {
    this.cleanupFns.set(name, fn);
  }
 
  // Track created entities for reporting
  trackEntity(type: string, entity: any): void {
    if (!this.createdEntities.has(type)) {
      this.createdEntities.set(type, []);
    }
    this.createdEntities.get(type)!.push(entity);
  }
 
  // Execute all registered cleanups
  async cleanup(): Promise<void> {
    const errors: string[] = [];
 
    for (const [name, fn] of this.cleanupFns) {
      try {
        await fn();
      } catch (error) {
        errors.push(`${name}: ${error}`);
      }
    }
 
    if (errors.length > 0) {
      console.warn('Cleanup warnings:', errors);
    }
 
    this.cleanupFns.clear();
    this.createdEntities.clear();
  }
 
  // Get summary of what was created
  getSummary(): Record<string, number> {
    const summary: Record<string, number> = {};
    for (const [type, entities] of this.createdEntities) {
      summary[type] = entities.length;
    }
    return summary;
  }
}

Using TestDefaults and TestDataHelper

Now our tests can generate unique data:

import { TestDefaults } from '../factories/TestDefaults';
import { TestDataHelper } from '../factories/TestDataHelper';
 
test('create user with defaults', async ({ request }) => {
  const helper = new TestDataHelper('User');
  
  const userData = {
    username: helper.uniqueName('john'),
    email: TestDefaults.internetEmail(),
    bio: TestDefaults.sentence(),
  };
 
  // Register cleanup
  helper.registerCleanup(`user-${userData.username}`, async () => {
    await deleteUser(userData.username);
  });
 
  const response = await request.post('/api/users', {
    data: userData,
  });
 
  expect(response.ok()).toBeTruthy();
  
  // Cleanup happens automatically via test.afterEach
});

What We've Learned

  • TestDefaults generates random, realistic test data
  • TestDataHelper manages unique naming and cleanup registration
  • These utilities are the foundation for both data-driven API cases and fluent seeding

Next: We'll use these utilities to stop hand-writing giant API specs.


Previous: Installing Playwright and Building Our Test Foundation Next: Stop Hand-Writing Giant API Specs ->