📝e2e-testing/03-testdefaults.md
The Foundation: TestDefaults & TestDataHelper
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 ->