Skip to content
Test Data Management logo

Test Data Management

> **Skill Purpose:** Test data creation, management, and fixture maintenance for testing scenarios

Coverage-Creatives/zeus0installs0stars

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:

  1. Define test data structure and organization
  2. Create scenario-specific test fixtures
  3. Set up test data generation and management
  4. Establish test data isolation and cleanup procedures
  5. 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