Test Data Management
> **Skill Purpose:** Test data creation, management, and fixture maintenance for testing scenarios
SKILL.md
Full skill instructions
Test Data Management
Skill Purpose: Test data creation, management, and fixture maintenance for testing scenarios
Core Skill Pattern
Objective: Establish comprehensive test data management patterns with fixture creation, scenario-specific data, and test environment synchronization.
Universal Pattern:
- Define test data structure and organization
- Create scenario-specific test fixtures
- Set up test data generation and management
- Establish test data isolation and cleanup procedures
- Create test data maintenance and versioning
Key Decisions (Project-Specific):
- Test data format and structure
- Fixture organization and categorization
- Data generation strategies and tools
- Isolation and cleanup procedures
- Maintenance and update frequency
Project-Specific Implementation Notes
Customize per project:
- Test data format based on testing framework and requirements
- Fixture organization based on test categories and scenarios
- Generation depth based on test complexity
- Isolation level based on test parallelization needs
- Maintenance frequency based on data volatility
Example Implementation (Test Data Management Pattern)
Note: This is an example pattern for test data management. Adapt test data format and procedures based on your specific testing framework and requirements.
Prerequisites (Example)
- Testing framework selected and configured
- Test scenarios documented and categorized
- Database testing environment established
- Test data requirements identified
Example: Test Data Management Implementation
Framework-Specific Example: This demonstrates test data management patterns using structured fixtures. Adapt for your testing framework and data requirements.
1. Test Data Structure
// Test data organization
interface TestFixture {
name: string;
description: string;
category: 'unit' | 'integration' | 'e2e' | 'performance';
scenario: string;
dependencies: string[];
tables: TableTestData[];
cleanup: CleanupStrategy;
setup: SetupProcedure[];
}
interface TableTestData {
tableName: string;
data: Record<string, any>[];
relationships?: RelationshipConfig[];
constraints?: ConstraintConfig[];
}
interface CleanupStrategy {
type: 'truncate' | 'delete' | 'rollback' | 'none';
tables?: string[];
order?: string[];
}
interface SetupProcedure {
type: 'sql' | 'function' | 'script';
description: string;
command: string;
timeout?: number;
}
// Example test fixture
const userRegistrationFixture: TestFixture = {
name: 'user_registration_flow',
description: 'Test data for user registration flow testing',
category: 'integration',
scenario: 'happy_path',
dependencies: ['base_users'],
tables: [
{
tableName: 'users',
data: [
{
id: 'test-user-001',
email: '[email protected]',
name: 'Test User',
role: 'user',
status: 'active',
created_at: '2024-01-01T00:00:00Z'
}
]
},
{
tableName: 'user_profiles',
data: [
{
user_id: 'test-user-001',
bio: 'Test user bio',
avatar_url: 'https://example.com/avatar.jpg',
preferences: '{"theme": "dark", "notifications": true}'
}
],
relationships: [
{
field: 'user_id',
referenceTable: 'users',
referenceField: 'id',
lookup: 'id'
}
]
}
],
cleanup: {
type: 'delete',
tables: ['user_profiles', 'users'],
order: ['user_profiles', 'users']
},
setup: [
{
type: 'sql',
description: 'Create test indexes',
command: 'CREATE INDEX IF NOT EXISTS idx_test_users_email ON users(email)',
timeout: 5000
}
]
};
2. Test Data Management System
// Test data execution and management
class TestDataManager {
private fixtures: Map<string, TestFixture> = new Map();
private activeFixtures: Set<string> = new Set();
private testData = new Map<string, Map<string, any>>();
constructor(private db: Database) {}
loadFixture(fixture: TestFixture): void {
this.fixtures.set(fixture.name, fixture);
}
async setupFixture(fixtureName: string): Promise<SetupResult> {
const fixture = this.fixtures.get(fixtureName);
if (!fixture) {
throw new Error(`Fixture ${fixtureName} not found`);
}
console.log(`🧪 Setting up test fixture: ${fixture.name}`);
const startTime = Date.now();
try {
// Check dependencies
await this.checkDependencies(fixture);
// Run setup procedures
await this.runSetupProcedures(fixture);
// Load test data
await this.loadTestData(fixture);
// Mark as active
this.activeFixtures.add(fixtureName);
return {
fixtureName,
success: true,
duration: Date.now() - startTime,
tablesLoaded: fixture.tables.length
};
} catch (error) {
return {
fixtureName,
success: false,
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async cleanupFixture(fixtureName: string): Promise<CleanupResult> {
const fixture = this.fixtures.get(fixtureName);
if (!fixture) {
throw new Error(`Fixture ${fixtureName} not found`);
}
if (!this.activeFixtures.has(fixtureName)) {
console.log(`⚠️ Fixture ${fixtureName} not active, skipping cleanup`);
return {
fixtureName,
success: true,
duration: 0,
tablesCleaned: 0
};
}
console.log(`🧹 Cleaning up test fixture: ${fixture.name}`);
const startTime = Date.now();
try {
await this.runCleanup(fixture);
// Remove from active fixtures
this.activeFixtures.delete(fixtureName);
// Clear test data cache
this.clearTestData(fixture);
return {
fixtureName,
success: true,
duration: Date.now() - startTime,
tablesCleaned: fixture.cleanup.tables?.length || 0
};
} catch (error) {
return {
fixtureName,
success: false,
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async cleanupAllFixtures(): Promise<CleanupAllResult> {
console.log('🧹 Cleaning up all active fixtures');
const results: CleanupResult[] = [];
// Clean up in reverse order of setup
const fixtureNames = Array.from(this.activeFixtures).reverse();
for (const fixtureName of fixtureNames) {
const result = await this.cleanupFixture(fixtureName);
results.push(result);
}
return {
totalFixtures: results.length,
successCount: results.filter(r => r.success).length,
failureCount: results.filter(r => !r.success).length,
results
};
}
private async checkDependencies(fixture: TestFixture): Promise<void> {
for (const dependency of fixture.dependencies) {
if (!this.activeFixtures.has(dependency)) {
console.log(`📦 Setting up dependency: ${dependency}`);
await this.setupFixture(dependency);
}
}
}
private async runSetupProcedures(fixture: TestFixture): Promise<void> {
for (const procedure of fixture.setup) {
console.log(`⚙️ Running setup procedure: ${procedure.description}`);
switch (procedure.type) {
case 'sql':
await this.db.query(procedure.command);
break;
case 'function':
await this.runFunction(procedure);
break;
case 'script':
await this.runScript(procedure);
break;
}
}
}
private async loadTestData(fixture: TestFixture): Promise<void> {
for (const tableData of fixture.tables) {
console.log(`📊 Loading test data for table: ${tableData.tableName}`);
// Store test data for potential retrieval
const tableCache = new Map<string, any>();
for (const record of tableData.data) {
// Process relationships
const processedRecord = await this.processTestRelationships(record, tableData.relationships);
// Insert test data
await this.insertTestData(tableData.tableName, processedRecord);
// Cache for retrieval
const cacheKey = this.generateCacheKey(processedRecord);
tableCache.set(cacheKey, processedRecord);
}
this.testData.set(tableData.tableName, tableCache);
}
}
private async processTestRelationships(record: Record<string, any>, relationships?: RelationshipConfig[]): Promise<Record<string, any>> {
if (!relationships) {
return record;
}
const processedRecord = { ...record };
for (const relationship of relationships) {
const referenceValue = processedRecord[relationship.field];
if (referenceValue) {
// Resolve relationship from test data cache
const resolvedValue = await this.resolveTestReference(relationship, referenceValue);
if (resolvedValue) {
processedRecord[relationship.field] = resolvedValue;
}
}
}
return processedRecord;
}
private async resolveTestReference(relationship: RelationshipConfig, value: string): Promise<any> {
const referenceCache = this.testData.get(relationship.referenceTable);
if (!referenceCache) {
throw new Error(`No test data cache found for ${relationship.referenceTable}`);
}
// Find reference by lookup method
for (const [cacheKey, record] of referenceCache) {
if (this.matchesLookup(record, relationship.referenceField, value, relationship.lookup)) {
return record.id;
}
}
throw new Error(`Cannot resolve reference ${value} in ${relationship.referenceTable}`);
}
private matchesLookup(record: any, field: string, value: string, lookup: string): boolean {
switch (lookup) {
case 'id':
return record[field] === value;
case 'email':
return record[field] === value;
case 'slug':
return record[field] === value;
default:
return record[field] === value;
}
}
private async insertTestData(tableName: string, data: Record<string, any>): Promise<void> {
const columns = Object.keys(data);
const values = Object.values(data);
const query = `
INSERT INTO ${tableName} (${columns.join(', ')})
VALUES (${columns.map((_, index) => `$${index + 1}`).join(', ')})
ON CONFLICT DO NOTHING
`;
await this.db.query(query, values);
}
private generateCacheKey(record: Record<string, any>): string {
// Generate a unique cache key for the record
return JSON.stringify(record);
}
private async runCleanup(fixture: TestFixture): Promise<void> {
switch (fixture.cleanup.type) {
case 'truncate':
await this.truncateTables(fixture.cleanup.tables || []);
break;
case 'delete':
await this.deleteTestData(fixture);
break;
case 'rollback':
await this.rollbackFixture(fixture);
break;
case 'none':
// No cleanup needed
break;
}
}
private async truncateTables(tables: string[]): Promise<void> {
for (const tableName of tables) {
await this.db.query(`TRUNCATE TABLE ${tableName} CASCADE`);
}
}
private async deleteTestData(fixture: TestFixture): Promise<void> {
const tables = fixture.cleanup.tables || [];
const order = fixture.cleanup.order || tables.reverse();
for (const tableName of order) {
// Delete only test data (data with test identifiers)
await this.db.query(`DELETE FROM ${tableName} WHERE id LIKE 'test-%'`);
}
}
private async rollbackFixture(fixture: TestFixture): Promise<void> {
// Implementation for rollback-based cleanup
console.log('🔄 Rolling back fixture changes');
}
private clearTestData(fixture: TestFixture): void {
for (const tableData of fixture.tables) {
this.testData.delete(tableData.tableName);
}
}
private async runFunction(procedure: SetupProcedure): Promise<void> {
// Implementation for function-based setup
console.log(`🔧 Running function: ${procedure.command}`);
}
private async runScript(procedure: SetupProcedure): Promise<void> {
// Implementation for script-based setup
console.log(`📜 Running script: ${procedure.command}`);
}
// Helper methods for test data access
getTestData(tableName: string, key?: string): any {
const tableCache = this.testData.get(tableName);
if (!tableCache) {
return null;
}
if (key) {
return tableCache.get(key);
}
return Array.from(tableCache.values());
}
getActiveFixtures(): string[] {
return Array.from(this.activeFixtures);
}
}
interface SetupResult {
fixtureName: string;
success: boolean;
duration: number;
tablesLoaded?: number;
error?: string;
}
interface CleanupResult {
fixtureName: string;
success: boolean;
duration: number;
tablesCleaned?: number;
error?: string;
}
interface CleanupAllResult {
totalFixtures: number;
successCount: number;
failureCount: number;
results: CleanupResult[];
}
3. Test Data Generation
// Test data generation utilities
interface DataGenerator {
name: string;
type: 'user' | 'content' | 'transaction' | 'custom';
template: Record<string, any>;
variations: number;
constraints?: GenerationConstraints;
}
interface GenerationConstraints {
unique?: string[];
ranges?: Record<string, [number, number]>;
enums?: Record<string, any[]>;
patterns?: Record<string, RegExp>;
}
class TestDataGenerator {
private generators: Map<string, DataGenerator> = new Map();
private usedValues = new Map<string, Set<any>>();
registerGenerator(generator: DataGenerator): void {
this.generators.set(generator.name, generator);
}
async generateData(generatorName: string, count: number = 1): Promise<Record<string, any>[]> {
const generator = this.generators.get(generatorName);
if (!generator) {
throw new Error(`Generator ${generatorName} not found`);
}
const results: Record<string, any>[] = [];
for (let i = 0; i < count; i++) {
const data = await this.generateSingle(generator, i);
results.push(data);
}
return results;
}
private async generateSingle(generator: DataGenerator, index: number): Promise<Record<string, any>> {
const data: Record<string, any> = { ...generator.template };
// Process each field
for (const [field, value] of Object.entries(data)) {
data[field] = await this.processField(field, value, generator, index);
}
// Apply constraints
if (generator.constraints) {
await this.applyConstraints(data, generator.constraints);
}
return data;
}
private async processField(field: string, value: any, generator: DataGenerator, index: number): Promise<any> {
if (typeof value === 'string' && value.startsWith('{{') && value.endsWith('}}')) {
const placeholder = value.slice(2, -2);
return await this.resolvePlaceholder(placeholder, field, generator, index);
}
return value;
}
private async resolvePlaceholder(placeholder: string, field: string, generator: DataGenerator, index: number): Promise<any> {
switch (placeholder) {
case 'uuid':
return `test-${generator.name}-${index}-${Date.now()}`;
case 'timestamp':
return new Date().toISOString();
case 'email':
return `test-${index}@example.com`;
case 'name':
return `Test ${generator.name} ${index}`;
case 'random':
return Math.random().toString(36).substring(7);
default:
if (placeholder.startsWith('range:')) {
return this.handleRangePlaceholder(placeholder);
}
if (placeholder.startsWith('enum:')) {
return this.handleEnumPlaceholder(placeholder, generator);
}
return placeholder;
}
}
private handleRangePlaceholder(placeholder: string): number {
const [_, range] = placeholder.split(':');
const [min, max] = range.split('-').map(Number);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
private handleEnumPlaceholder(placeholder: string, generator: DataGenerator): any {
const [_, enumName] = placeholder.split(':');
const enumValues = generator.constraints?.enums?.[enumName];
if (!enumValues) {
throw new Error(`Enum ${enumName} not defined in constraints`);
}
return enumValues[Math.floor(Math.random() * enumValues.length)];
}
private async applyConstraints(data: Record<string, any>, constraints: GenerationConstraints): Promise<void> {
// Handle uniqueness constraints
if (constraints.unique) {
for (const field of constraints.unique) {
await this.ensureUniqueness(field, data[field], field);
}
}
// Handle range constraints
if (constraints.ranges) {
for (const [field, [min, max]] of Object.entries(constraints.ranges)) {
if (data[field] < min || data[field] > max) {
data[field] = Math.floor(Math.random() * (max - min + 1)) + min;
}
}
}
// Handle pattern constraints
if (constraints.patterns) {
for (const [field, pattern] of Object.entries(constraints.patterns)) {
if (!pattern.test(data[field])) {
data[field] = this.generatePatternMatch(pattern);
}
}
}
}
private async ensureUniqueness(field: string, value: any, context: string): Promise<void> {
const uniqueKey = `${context}:${field}`;
const usedSet = this.usedValues.get(uniqueKey) || new Set();
if (usedSet.has(value)) {
// Generate new unique value
let attempts = 0;
let newValue = value;
while (usedSet.has(newValue) && attempts < 100) {
newValue = `${value}-${attempts}`;
attempts++;
}
if (usedSet.has(newValue)) {
throw new Error(`Cannot generate unique value for ${field} in ${context}`);
}
value = newValue;
}
usedSet.add(value);
this.usedValues.set(uniqueKey, usedSet);
}
private generatePatternMatch(pattern: RegExp): string {
// Generate a string that matches the pattern
// This is a simplified implementation
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 10; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
}
// Example generators
const userGenerator: DataGenerator = {
name: 'user',
type: 'user',
template: {
id: '{{uuid}}',
email: '{{email}}',
name: '{{name}}',
role: 'user',
status: 'active',
created_at: '{{timestamp}}'
},
variations: 10,
constraints: {
unique: ['email'],
enums: {
role: ['user', 'admin', 'moderator']
}
}
};
const contentGenerator: DataGenerator = {
name: 'content',
type: 'content',
template: {
id: '{{uuid}}',
title: 'Test Content {{random}}',
content: 'This is test content with some random text {{random}}',
status: 'published',
created_at: '{{timestamp}}'
},
variations: 5,
constraints: {
ranges: {
content_length: [50, 500]
}
}
};
4. Test Data Isolation
// Test data isolation for parallel testing
interface IsolationStrategy {
type: 'database' | 'schema' | 'transaction' | 'namespace';
implementation: IsolationImplementation;
}
interface IsolationImplementation {
setup: () => Promise<void>;
cleanup: () => Promise<void>;
getConnection: () => Promise<any>;
}
class TestIsolationManager {
private strategies: Map<string, IsolationStrategy> = new Map();
private activeIsolations: Map<string, any> = new Map();
registerStrategy(name: string, strategy: IsolationStrategy): void {
this.strategies.set(name, strategy);
}
async createIsolation(strategyName: string, testId: string): Promise<IsolationResult> {
const strategy = this.strategies.get(strategyName);
if (!strategy) {
throw new Error(`Isolation strategy ${strategyName} not found`);
}
console.log(`🔒 Creating isolation for test ${testId} using ${strategyName}`);
try {
await strategy.implementation.setup();
const connection = await strategy.implementation.getConnection();
// Store active isolation
this.activeIsolations.set(testId, {
strategy: strategyName,
connection,
cleanup: strategy.implementation.cleanup
});
return {
testId,
strategy: strategyName,
success: true,
connection
};
} catch (error) {
return {
testId,
strategy: strategyName,
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async cleanupIsolation(testId: string): Promise<void> {
const isolation = this.activeIsolations.get(testId);
if (!isolation) {
console.log(`⚠️ No active isolation found for test ${testId}`);
return;
}
console.log(`🔓 Cleaning up isolation for test ${testId}`);
try {
await isolation.cleanup();
this.activeIsolations.delete(testId);
} catch (error) {
console.error(`Error cleaning up isolation for ${testId}:`, error);
}
}
async cleanupAllIsolations(): Promise<void> {
console.log('🔓 Cleaning up all active isolations');
const testIds = Array.from(this.activeIsolations.keys());
for (const testId of testIds) {
await this.cleanupIsolation(testId);
}
}
getConnection(testId: string): any {
const isolation = this.activeIsolations.get(testId);
return isolation?.connection;
}
}
interface IsolationResult {
testId: string;
strategy: string;
success: boolean;
connection?: any;
error?: string;
}
// Example isolation strategies
const databaseIsolation: IsolationStrategy = {
type: 'database',
implementation: {
setup: async () => {
// Create test-specific database
console.log('🗄️ Setting up test database');
},
cleanup: async () => {
// Drop test database
console.log('🗑️ Cleaning up test database');
},
getConnection: async () => {
// Return test database connection
return {}; // Database connection
}
}
};
const transactionIsolation: IsolationStrategy = {
type: 'transaction',
implementation: {
setup: async () => {
// Begin transaction
console.log('🔄 Beginning test transaction');
},
cleanup: async () => {
// Rollback transaction
console.log('🔙 Rolling back test transaction');
},
getConnection: async () => {
// Return transaction connection
return {}; // Transaction connection
}
}
};
Integration Patterns
QA Department Integration
- Test data provision for automated tests
- Environment synchronization
- Test scenario data management
Development Integration
- Local development test data
- Debug data provision
- Rapid test setup
CI/CD Integration
- Automated test data setup
- Parallel test isolation
- Test environment cleanup
Stop Conditions
STOP test data operations and escalate if:
- Test data corruption detected
- Isolation failures occur
- Data conflicts between tests
- Performance degradation observed
- Memory or storage limits exceeded
PAUSE operations if:
- Database connections exhausted
- High test parallelization
- Resource constraints
- Backup operations in progress
Best Practices
Data Organization:
- Clear fixture categorization
- Scenario-specific data
- Proper dependency management
- Consistent naming conventions
Data Quality:
- Realistic test data
- Proper data relationships
- Edge case coverage
- Performance-appropriate volumes
Isolation Management:
- Proper test isolation
- Efficient cleanup procedures
- Resource management
- Parallel test support
