Last updated: 3 min ago

Testing and Debugging Regex Patterns: Complete Developer's Guide

Master regex testing, debugging, and validation with comprehensive tools, techniques, and best practices for bulletproof pattern development.

Testing and Debugging Regex Patterns: Complete Developer's Guide

Writing a regex pattern is only half the battle. Ensuring it works correctly across all edge cases, performs well, and remains maintainable requires systematic testing and debugging. This comprehensive guide covers everything you need to know about testing regex patterns like a professional developer.

Table of Contents

  1. Testing Fundamentals
  2. Debugging Techniques and Tools
  3. Test Case Design and Coverage
  4. Automated Testing Frameworks
  5. Performance Testing and Benchmarking
  6. Security Testing and ReDoS Prevention
  7. Cross-Platform and Browser Testing
  8. Maintenance and Monitoring
  9. Advanced Debugging Tools and Techniques

Testing Fundamentals

Why Test Regex Patterns?

Regex patterns are notoriously difficult to get right. Common issues include:

  • False positives: Matching strings that shouldn't match
  • False negatives: Failing to match valid strings
  • Performance problems: Catastrophic backtracking causing timeouts
  • Edge case failures: Not handling boundary conditions
  • Maintainability issues: Complex patterns becoming unmanageable

Testing Philosophy

// Test-Driven Development for Regex
class RegexTDD {
  // 1. Start with test cases
  static defineRequirements() {
    return {
      shouldMatch: [
        'user@example.com',
        'test.email+tag@domain.co.uk',
        'firstname.lastname@company.org'
      ],
      shouldNotMatch: [
        'plainaddress',
        '@domain.com',
        'user@',
        'user..name@domain.com',
        'user@domain'
      ]
    };
  }
  
  // 2. Write failing tests first
  static testPattern(pattern, requirements) {
    const regex = new RegExp(pattern);
    const results = {
      passedPositive: 0,
      passedNegative: 0,
      failedPositive: [],
      failedNegative: []
    };
    
    // Test positive cases
    requirements.shouldMatch.forEach(input => {
      if (regex.test(input)) {
        results.passedPositive++;
      } else {
        results.failedPositive.push(input);
      }
    });
    
    // Test negative cases
    requirements.shouldNotMatch.forEach(input => {
      if (!regex.test(input)) {
        results.passedNegative++;
      } else {
        results.failedNegative.push(input);
      }
    });
    
    return results;
  }
  
  // 3. Iterate until all tests pass
  static developPattern() {
    const requirements = this.defineRequirements();
    const patterns = [
      // Evolution of email pattern
      /.*@.*\..*/, // Too broad
      /\w+@\w+\.\w+/, // Too restrictive
      /[\w._%+-]+@[\w.-]+\.[\w]{2,}/, // Better
      /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ // Final
    ];
    
    patterns.forEach((pattern, index) => {
      console.log(`\nTesting pattern ${index + 1}: ${pattern}`);
      const results = this.testPattern(pattern.source, requirements);
      console.log(`Positive tests passed: ${results.passedPositive}/${requirements.shouldMatch.length}`);
      console.log(`Negative tests passed: ${results.passedNegative}/${requirements.shouldNotMatch.length}`);
      
      if (results.failedPositive.length > 0) {
        console.log('Failed positive cases:', results.failedPositive);
      }
      if (results.failedNegative.length > 0) {
        console.log('Failed negative cases:', results.failedNegative);
      }
    });
  }
}

// Run the TDD process
RegexTDD.developPattern();

Basic Testing Framework

class SimpleRegexTester {
  constructor(pattern, flags = '') {
    this.pattern = pattern;
    this.regex = new RegExp(pattern, flags);
    this.tests = [];
  }
  
  // Add test cases
  shouldMatch(...inputs) {
    inputs.forEach(input => {
      this.tests.push({
        input,
        expected: true,
        type: 'positive'
      });
    });
    return this;
  }
  
  shouldNotMatch(...inputs) {
    inputs.forEach(input => {
      this.tests.push({
        input,
        expected: false,
        type: 'negative'
      });
    });
    return this;
  }
  
  // Run all tests
  run() {
    console.log(`Testing pattern: /${this.pattern}/${this.regex.flags}`);
    console.log('='.repeat(60));
    
    let passed = 0;
    let failed = 0;
    const failures = [];
    
    this.tests.forEach((test, index) => {
      const actual = this.regex.test(test.input);
      const success = actual === test.expected;
      
      if (success) {
        passed++;
        console.log(`✓ Test ${index + 1}: "${test.input}" (${test.type})`);
      } else {
        failed++;
        failures.push(test);
        console.log(`✗ Test ${index + 1}: "${test.input}" (${test.type})`);
        console.log(`  Expected: ${test.expected}, Got: ${actual}`);
      }
    });
    
    console.log('\nSummary:');
    console.log(`Passed: ${passed}`);
    console.log(`Failed: ${failed}`);
    console.log(`Success rate: ${(passed / this.tests.length * 100).toFixed(1)}%`);
    
    return {
      passed,
      failed,
      total: this.tests.length,
      successRate: passed / this.tests.length,
      failures
    };
  }
}

// Usage example
const emailTester = new SimpleRegexTester(
  '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$'
);

const results = emailTester
  .shouldMatch(
    'user@example.com',
    'test.email+tag@domain.org',
    'firstname.lastname@company.co.uk'
  )
  .shouldNotMatch(
    'plainaddress',
    '@domain.com',
    'user@',
    'user@domain'
  )
  .run();

Debugging Techniques and Tools

Step-by-Step Pattern Breakdown

class RegexDebugger {
  static breakdownPattern(pattern, testString) {
    console.log(`Debugging pattern: ${pattern}`);
    console.log(`Test string: "${testString}"`);
    console.log('='.repeat(50));
    
    // Break down complex pattern into parts
    const parts = this.parsePattern(pattern);
    
    parts.forEach((part, index) => {
      try {
        const partRegex = new RegExp(part.pattern);
        const matches = partRegex.test(testString);
        const status = matches ? '✓' : '✗';
        
        console.log(`${status} Part ${index + 1}: /${part.pattern}/`);
        console.log(`  Description: ${part.description}`);
        console.log(`  Matches: ${matches}`);
        
        if (matches) {
          const match = testString.match(partRegex);
          if (match) {
            console.log(`  Match: "${match[0]}" at position ${match.index}`);
          }
        }
        console.log();
      } catch (error) {
        console.log(`✗ Part ${index + 1}: /${part.pattern}/`);
        console.log(`  Error: ${error.message}`);
        console.log();
      }
    });
  }
  
  static parsePattern(pattern) {
    // This is a simplified pattern parser
    // In a real implementation, you'd have a more sophisticated parser
    const parts = [];
    
    // Example: breaking down email pattern
    if (pattern.includes('@')) {
      // Extract local part pattern
      const localMatch = pattern.match(/\^?([^@]+)@/);
      if (localMatch) {
        parts.push({
          pattern: localMatch[1],
          description: 'Local part (before @)'
        });
      }
      
      // Extract @ symbol
      parts.push({
        pattern: '@',
        description: 'At symbol'
      });
      
      // Extract domain part pattern
      const domainMatch = pattern.match(/@([^$]+)\$?/);
      if (domainMatch) {
        parts.push({
          pattern: domainMatch[1],
          description: 'Domain part (after @)'
        });
      }
    } else {
      // For non-email patterns, return the whole pattern
      parts.push({
        pattern: pattern.replace(/^\^|\$$/g, ''),
        description: 'Full pattern'
      });
    }
    
    return parts;
  }
  
  // Visualize regex execution
  static visualizeExecution(pattern, testString) {
    console.log('Regex Execution Visualization');
    console.log('='.repeat(40));
    console.log(`Pattern: /${pattern}/`);
    console.log(`Input: "${testString}"`);
    console.log();
    
    const regex = new RegExp(pattern, 'g');
    let match;
    let position = 0;
    let matchCount = 0;
    
    while ((match = regex.exec(testString)) !== null) {
      matchCount++;
      
      // Show the match in context
      const before = testString.substring(0, match.index);
      const matched = match[0];
      const after = testString.substring(match.index + matched.length);
      
      console.log(`Match ${matchCount}:`);
      console.log(`  Position: ${match.index}-${match.index + matched.length - 1}`);
      console.log(`  Matched: "${matched}"`);
      console.log(`  Context: "${before}[${matched}]${after}"`);
      
      if (match.length > 1) {
        console.log('  Captured groups:');
        for (let i = 1; i < match.length; i++) {
          console.log(`    Group ${i}: "${match[i]}"`);
        }
      }
      
      console.log();
      
      // Prevent infinite loops
      if (regex.lastIndex === match.index) {
        regex.lastIndex++;
      }
    }
    
    if (matchCount === 0) {
      console.log('No matches found.');
    }
    
    return matchCount;
  }
  
  // Common regex mistakes detector
  static detectCommonMistakes(pattern) {
    const mistakes = [];
    
    // Unescaped special characters
    const specialChars = ['.', '+', '*', '?', '^', '$', '(', ')', '[', ']', '{', '}', '|', '\\'];
    specialChars.forEach(char => {
      if (pattern.includes(char) && !pattern.includes('\\' + char) && char !== '\\') {
        if (pattern.indexOf(char) !== pattern.lastIndexOf(char) || 
            (char === '.' && !pattern.includes('\\.'))) {
          mistakes.push({
            type: 'unescaped_special_char',
            message: `Unescaped special character '${char}' may not match literally`,
            suggestion: `Use \\${char} to match the literal character`
          });
        }
      }
    });
    
    // Potential ReDoS patterns
    if (/(\(.+\)\+|\(.+\)\*).*(\(.+\)\+|\(.+\)\*)/.test(pattern)) {
      mistakes.push({
        type: 'potential_redos',
        message: 'Pattern may be vulnerable to ReDoS (catastrophic backtracking)',
        suggestion: 'Consider using possessive quantifiers or atomic groups'
      });
    }
    
    // Unnecessary capturing groups
    const capturingGroups = (pattern.match(/\([^?]/g) || []).length;
    const nonCapturingGroups = (pattern.match(/\(\?:/g) || []).length;
    
    if (capturingGroups > 2 && nonCapturingGroups === 0) {
      mistakes.push({
        type: 'unnecessary_capturing',
        message: 'Multiple capturing groups detected',
        suggestion: 'Use (?:) for groups you don\'t need to capture for better performance'
      });
    }
    
    // Missing anchors for validation
    if (!pattern.startsWith('^') && !pattern.endsWith('$')) {
      mistakes.push({
        type: 'missing_anchors',
        message: 'Pattern lacks anchors for exact matching',
        suggestion: 'Add ^ and $ for validation patterns'
      });
    }
    
    return mistakes;
  }
}

// Usage examples
RegexDebugger.breakdownPattern(
  '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
  'user@example.com'
);

RegexDebugger.visualizeExecution(
  '\\d{1,3}',
  'Address: 123 Main St, Apt 45, ZIP 12345'
);

const mistakes = RegexDebugger.detectCommonMistakes('(.+)+@(.+)+\\.(..)');
console.log('Potential issues:', mistakes);

Interactive Debugging Console

class InteractiveRegexDebugger {
  constructor() {
    this.history = [];
    this.currentPattern = '';
    this.testStrings = [];
  }
  
  // Set the pattern to debug
  setPattern(pattern, flags = '') {
    this.currentPattern = pattern;
    this.regex = new RegExp(pattern, flags);
    console.log(`Pattern set: /${pattern}/${flags}`);
    return this;
  }
  
  // Add test strings
  addTest(...strings) {
    this.testStrings.push(...strings);
    console.log(`Added ${strings.length} test strings`);
    return this;
  }
  
  // Test current pattern against all test strings
  test() {
    if (!this.regex) {
      console.log('No pattern set. Use setPattern() first.');
      return this;
    }
    
    console.log('\nTesting Results:');
    console.log('='.repeat(40));
    
    this.testStrings.forEach((str, index) => {
      const match = str.match(this.regex);
      if (match) {
        console.log(`✓ Test ${index + 1}: "${str}"`);
        console.log(`  Match: "${match[0]}"`);
        if (match.length > 1) {
          console.log(`  Groups: [${match.slice(1).map(g => `"${g}"`).join(', ')}]`);
        }
      } else {
        console.log(`✗ Test ${index + 1}: "${str}" - No match`);
      }
    });
    
    return this;
  }
  
  // Find matches with context
  findMatches(includeGlobal = true) {
    if (!this.regex) {
      console.log('No pattern set. Use setPattern() first.');
      return this;
    }
    
    const globalRegex = new RegExp(this.currentPattern, 
      this.regex.flags.includes('g') ? this.regex.flags : this.regex.flags + 'g'
    );
    
    console.log('\nAll Matches:');
    console.log('='.repeat(40));
    
    this.testStrings.forEach((str, strIndex) => {
      console.log(`\nString ${strIndex + 1}: "${str}"`);
      
      let match;
      let matchCount = 0;
      globalRegex.lastIndex = 0; // Reset for each string
      
      while ((match = globalRegex.exec(str)) !== null) {
        matchCount++;
        const start = Math.max(0, match.index - 10);
        const end = Math.min(str.length, match.index + match[0].length + 10);
        const context = str.substring(start, end);
        const highlightedContext = context.replace(
          match[0],
          `[${match[0]}]`
        );
        
        console.log(`  Match ${matchCount}: "${match[0]}" at ${match.index}`);
        console.log(`  Context: ...${highlightedContext}...`);
        
        if (globalRegex.lastIndex === match.index) {
          globalRegex.lastIndex++;
        }
      }
      
      if (matchCount === 0) {
        console.log('  No matches found');
      }
    });
    
    return this;
  }
  
  // Performance testing
  benchmark(iterations = 10000) {
    if (!this.regex || this.testStrings.length === 0) {
      console.log('Need pattern and test strings for benchmarking.');
      return this;
    }
    
    console.log(`\nBenchmarking ${iterations} iterations...`);
    
    const start = performance.now();
    
    for (let i = 0; i < iterations; i++) {
      this.testStrings.forEach(str => {
        this.regex.test(str);
      });
    }
    
    const end = performance.now();
    const duration = end - start;
    const opsPerSecond = (iterations * this.testStrings.length) / (duration / 1000);
    
    console.log(`Duration: ${duration.toFixed(2)}ms`);
    console.log(`Operations/second: ${opsPerSecond.toFixed(0)}`);
    console.log(`Average per operation: ${(duration / (iterations * this.testStrings.length)).toFixed(4)}ms`);
    
    return this;
  }
  
  // Show regex explanation (simplified)
  explain() {
    if (!this.currentPattern) {
      console.log('No pattern set.');
      return this;
    }
    
    console.log('\nPattern Explanation:');
    console.log('='.repeat(40));
    console.log(`Pattern: /${this.currentPattern}/${this.regex.flags}`);
    
    // Basic explanation - in a real implementation, you'd have a full parser
    const explanations = {
      '^': 'Start of string',
      '$': 'End of string',
      '.': 'Any character (except newline)',
      '*': 'Zero or more of previous',
      '+': 'One or more of previous',
      '?': 'Zero or one of previous',
      '\\d': 'Digit (0-9)',
      '\\w': 'Word character (a-zA-Z0-9_)',
      '\\s': 'Whitespace character',
      '\\D': 'Non-digit',
      '\\W': 'Non-word character',
      '\\S': 'Non-whitespace',
      '[]': 'Character class',
      '()': 'Capturing group',
      '(?:)': 'Non-capturing group',
      '|': 'Alternation (OR)'
    };
    
    for (const [symbol, meaning] of Object.entries(explanations)) {
      if (this.currentPattern.includes(symbol)) {
        console.log(`${symbol.padEnd(8)} - ${meaning}`);
      }
    }
    
    return this;
  }
  
  // Clear test strings
  clear() {
    this.testStrings = [];
    console.log('Test strings cleared.');
    return this;
  }
}

// Usage example
const debugger = new InteractiveRegexDebugger()
  .setPattern('^\\d{3}-\\d{2}-\\d{4}$')
  .addTest('123-45-6789', '123456789', '123-45-67890', 'abc-de-fghi')
  .explain()
  .test()
  .benchmark(1000);

Test Case Design and Coverage

Comprehensive Test Case Categories

class TestCaseDesigner {
  static generateEmailTestCases() {
    return {
      // Valid cases - should match
      validCases: [
        // Basic formats
        'user@example.com',
        'test@domain.org',
        'admin@site.net',
        
        // With special characters
        'user.name@domain.com',
        'user+tag@domain.com',
        'user_underscore@domain.com',
        'user-hyphen@domain.com',
        'user%percent@domain.com',
        
        // Multiple dots and hyphens
        'first.last@sub.domain.com',
        'user@multi-word-domain.com',
        
        // International domains
        'user@domain.co.uk',
        'test@example.info',
        'admin@site.museum',
        
        // Edge cases
        'a@b.co', // Minimal valid email
        'very.long.email.address@very.long.domain.name.com',
        '1234567890@example.com', // Numeric local part
        'user@192.168.1.1' // IP address domain (if supported)
      ],
      
      // Invalid cases - should NOT match
      invalidCases: [
        // Missing parts
        '',
        'plainaddress',
        '@domain.com',
        'user@',
        'user.domain.com',
        
        // Invalid characters
        'user name@domain.com', // Space in local part
        'user@domain .com', // Space in domain
        'user@domain..com', // Double dots
        'user..name@domain.com', // Double dots in local
        
        // Invalid format
        'user@@domain.com', // Double @
        'user@domain@com', // Multiple @
        '.user@domain.com', // Starting with dot
        'user.@domain.com', // Ending with dot
        'user@.domain.com', // Domain starting with dot
        'user@domain.', // Domain ending with dot
        
        // Missing TLD
        'user@domain',
        'user@localhost',
        
        // Too long
        'a'.repeat(65) + '@domain.com', // Local part too long
        'user@' + 'a'.repeat(250) + '.com', // Domain too long
        
        // Invalid TLD
        'user@domain.c', // TLD too short
        'user@domain.123' // Numeric TLD
      ],
      
      // Edge cases requiring special attention
      edgeCases: [
        'user+tag+extra@domain.com',
        'user@sub1.sub2.domain.com',
        'user@domain-with-hyphens.com',
        'user@xn--domain.com', // Punycode
        'test@[192.168.1.1]' // IP in brackets
      ]
    };
  }
  
  static generatePhoneTestCases() {
    return {
      validCases: [
        // Standard US format
        '(555) 123-4567',
        '555-123-4567',
        '555.123.4567',
        '5551234567',
        '+1 555 123 4567',
        '+1-555-123-4567',
        
        // Edge cases
        '(800) 555-1212', // Toll-free
        '911', // Emergency (if supported)
        '411' // Information (if supported)
      ],
      
      invalidCases: [
        // Wrong format
        '123-456-78901', // Too many digits
        '123-456-789', // Too few digits
        '(123) 456-789', // Missing digit
        '1234-567-890', // Wrong grouping
        
        // Invalid area codes
        '(000) 123-4567', // 000 area code
        '(123) 012-3456', // 0xx exchange
        '(123) 112-3456', // 1xx exchange
        
        // Invalid characters
        'abc-def-ghij',
        '555-123-ABCD',
        '555 123 4567 ext 123' // Extensions
      ]
    };
  }
  
  static generatePasswordTestCases() {
    return {
      validCases: [
        'Password123!',
        'MySecure1Pass@',
        'Strong&Password9',
        '8CharP@ss',
        'VeryLongPasswordWithNumbers123AndSymbols!@#'
      ],
      
      invalidCases: [
        // Too short
        'Pass1!',
        '1234567',
        
        // Missing requirements
        'password123!', // No uppercase
        'PASSWORD123!', // No lowercase
        'Password!', // No numbers
        'Password123', // No special chars
        
        // Common patterns
        'Password123',
        'qwerty123',
        '123456789',
        'password',
        
        // All same character
        'aaaaaaaa',
        '11111111'
      ]
    };
  }
  
  // Generate boundary tests
  static generateBoundaryTests(pattern, validSample) {
    const tests = {
      lengthTests: [],
      characterTests: [],
      formatTests: []
    };
    
    // Length boundary tests
    for (let i = 1; i <= validSample.length + 5; i++) {
      const testStr = validSample.substring(0, i);
      tests.lengthTests.push({
        input: testStr,
        length: i,
        description: `Length ${i} test`
      });
    }
    
    // Character boundary tests
    const chars = '!@#$%^&*()[]{}|\\;:",.<>?/~`+-=';
    chars.split('').forEach(char => {
      tests.characterTests.push({
        input: validSample + char,
        char: char,
        description: `With special char '${char}'`
      });
    });
    
    // Format tests - modify valid sample
    tests.formatTests = [
      { input: ' ' + validSample, description: 'Leading space' },
      { input: validSample + ' ', description: 'Trailing space' },
      { input: validSample.toUpperCase(), description: 'All uppercase' },
      { input: validSample.toLowerCase(), description: 'All lowercase' },
      { input: validSample.repeat(2), description: 'Doubled' }
    ];
    
    return tests;
  }
}

// Usage
const emailTests = TestCaseDesigner.generateEmailTestCases();
console.log('Email test cases generated:');
console.log(`Valid: ${emailTests.validCases.length}`);
console.log(`Invalid: ${emailTests.invalidCases.length}`);
console.log(`Edge cases: ${emailTests.edgeCases.length}`);

Coverage Analysis

class CoverageAnalyzer {
  static analyzePattern(pattern) {
    const analysis = {
      complexity: this.calculateComplexity(pattern),
      features: this.extractFeatures(pattern),
      riskAreas: this.identifyRiskAreas(pattern),
      testingSuggestions: []
    };
    
    // Generate testing suggestions based on analysis
    analysis.testingSuggestions = this.generateSuggestions(analysis);
    
    return analysis;
  }
  
  static calculateComplexity(pattern) {
    let score = 0;
    
    // Count different regex features
    const features = {
      characterClasses: (pattern.match(/\[[^\]]+\]/g) || []).length,
      quantifiers: (pattern.match(/[+*?{]/g) || []).length,
      groups: (pattern.match(/\(/g) || []).length,
      anchors: (pattern.match(/[^$]/g) || []).length,
      alternations: (pattern.match(/\|/g) || []).length,
      lookarounds: (pattern.match(/\(\?[=!<]/g) || []).length
    };
    
    // Weight different features
    score += features.characterClasses * 2;
    score += features.quantifiers * 3;
    score += features.groups * 2;
    score += features.anchors * 1;
    score += features.alternations * 4;
    score += features.lookarounds * 5;
    
    return {
      score,
      level: score < 10 ? 'simple' : score < 25 ? 'moderate' : 'complex',
      features
    };
  }
  
  static extractFeatures(pattern) {
    return {
      hasAnchors: /^\^|\$$/.test(pattern),
      hasCharacterClasses: /\[[^\]]+\]/.test(pattern),
      hasQuantifiers: /[+*?{}]/.test(pattern),
      hasGroups: /\(/.test(pattern),
      hasAlternation: /\|/.test(pattern),
      hasLookarounds: /\(\?[=!<]/.test(pattern),
      hasEscaping: /\\/.test(pattern),
      hasFlags: false // Would need to check regex flags separately
    };
  }
  
  static identifyRiskAreas(pattern) {
    const risks = [];
    
    // Check for ReDoS patterns
    if (/(\(.+\)[+*]){2,}/.test(pattern)) {
      risks.push({
        type: 'redos',
        severity: 'high',
        message: 'Potential catastrophic backtracking',
        pattern: 'Multiple quantified groups'
      });
    }
    
    // Check for overly broad patterns
    if (/\.\*/.test(pattern) && !/\^\.\*\$$/.test(pattern)) {
      risks.push({
        type: 'broad_matching',
        severity: 'medium',
        message: 'Pattern may match too broadly',
        pattern: '.*'
      });
    }
    
    // Check for unescaped special characters
    const unescapedSpecial = pattern.match(/(? 3) {
      risks.push({
        type: 'unescaped_special',
        severity: 'low',
        message: 'Many unescaped special characters',
        pattern: unescapedSpecial.join(', ')
      });
    }
    
    return risks;
  }
  
  static generateSuggestions(analysis) {
    const suggestions = [];
    
    // Complexity-based suggestions
    if (analysis.complexity.level === 'complex') {
      suggestions.push({
        category: 'testing',
        message: 'Complex pattern requires extensive edge case testing',
        priority: 'high'
      });
    }
    
    // Feature-based suggestions
    if (analysis.features.hasQuantifiers) {
      suggestions.push({
        category: 'testing',
        message: 'Test boundary conditions for quantifiers (0, 1, many)',
        priority: 'medium'
      });
    }
    
    if (analysis.features.hasCharacterClasses) {
      suggestions.push({
        category: 'testing',
        message: 'Test characters at boundaries of character classes',
        priority: 'medium'
      });
    }
    
    if (analysis.features.hasAlternation) {
      suggestions.push({
        category: 'testing',
        message: 'Test all branches of alternation patterns',
        priority: 'high'
      });
    }
    
    // Risk-based suggestions
    analysis.riskAreas.forEach(risk => {
      if (risk.type === 'redos') {
        suggestions.push({
          category: 'performance',
          message: 'Test with long strings to check for ReDoS vulnerability',
          priority: 'critical'
        });
      }
    });
    
    return suggestions;
  }
  
  // Test coverage checker
  static checkCoverage(pattern, testCases) {
    const analysis = this.analyzePattern(pattern);
    const coverage = {
      totalFeatures: Object.keys(analysis.features).length,
      testedFeatures: 0,
      missingTests: [],
      recommendations: []
    };
    
    // Check if test cases cover different pattern features
    const regex = new RegExp(pattern);
    
    // Test anchoring
    if (analysis.features.hasAnchors) {
      const anchorTests = testCases.filter(test => {
        const fullMatch = regex.test(test.input);
        const partialPattern = pattern.replace(/^\^|\$$/g, '');
        const partialRegex = new RegExp(partialPattern);
        const partialMatch = partialRegex.test(test.input);
        return fullMatch !== partialMatch;
      });
      
      if (anchorTests.length > 0) {
        coverage.testedFeatures++;
      } else {
        coverage.missingTests.push('anchor behavior');
      }
    }
    
    // Check quantifier boundaries
    if (analysis.features.hasQuantifiers) {
      const quantifierTests = testCases.filter(test => {
        return test.description && test.description.includes('boundary');
      });
      
      if (quantifierTests.length > 0) {
        coverage.testedFeatures++;
      } else {
        coverage.missingTests.push('quantifier boundaries');
      }
    }
    
    coverage.percentage = (coverage.testedFeatures / coverage.totalFeatures) * 100;
    
    return coverage;
  }
}

// Usage
const pattern = '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$';
const analysis = CoverageAnalyzer.analyzePattern(pattern);

console.log('Pattern Analysis:');
console.log(`Complexity: ${analysis.complexity.level} (score: ${analysis.complexity.score})`);
console.log('Features:', analysis.features);
console.log('Risk areas:', analysis.riskAreas);
console.log('Suggestions:', analysis.testingSuggestions);

Automated Testing Frameworks

Jest Integration

// regex-patterns.js
const RegexPatterns = {
  email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
  phone: /^\(?[2-9]\d{2}\)?[-.\s]?[2-9]\d{2}[-.\s]?\d{4}$/,
  strongPassword: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$/,
  creditCard: /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})$/
};

module.exports = RegexPatterns;

// regex-patterns.test.js
const RegexPatterns = require('./regex-patterns');

describe('Email Validation', () => {
  const emailRegex = RegexPatterns.email;
  
  describe('Valid emails', () => {
    test.each([
      'user@example.com',
      'test.email+tag@domain.org',
      'user_name@domain-name.com',
      'firstname.lastname@company.co.uk',
      'user123@domain123.net'
    ])('should match valid email: %s', (email) => {
      expect(emailRegex.test(email)).toBe(true);
    });
  });
  
  describe('Invalid emails', () => {
    test.each([
      'plainaddress',
      '@domain.com',
      'user@',
      'user.domain.com',
      'user@domain',
      'user..name@domain.com',
      'user@domain..com',
      '.user@domain.com',
      'user.@domain.com'
    ])('should not match invalid email: %s', (email) => {
      expect(emailRegex.test(email)).toBe(false);
    });
  });
  
  describe('Edge cases', () => {
    test('should handle minimum valid email', () => {
      expect(emailRegex.test('a@b.co')).toBe(true);
    });
    
    test('should reject email with space', () => {
      expect(emailRegex.test('user name@domain.com')).toBe(false);
    });
    
    test('should reject email without TLD', () => {
      expect(emailRegex.test('user@localhost')).toBe(false);
    });
  });
});

describe('Performance Tests', () => {
  test('email regex should not cause ReDoS', () => {
    const maliciousInput = 'a'.repeat(50000) + 'X';
    const start = Date.now();
    
    RegexPatterns.email.test(maliciousInput);
    
    const duration = Date.now() - start;
    expect(duration).toBeLessThan(100); // Should complete within 100ms
  });
  
  test('should handle large valid emails efficiently', () => {
    const longEmail = 'a'.repeat(50) + '@' + 'b'.repeat(50) + '.com';
    const start = Date.now();
    
    RegexPatterns.email.test(longEmail);
    
    const duration = Date.now() - start;
    expect(duration).toBeLessThan(10); // Should be very fast
  });
});

describe('Cross-browser compatibility', () => {
  test('regex should work consistently across environments', () => {
    const testEmail = 'user@example.com';
    
    // Test with different regex creation methods
    const literal = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    const constructor = new RegExp('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$');
    
    expect(literal.test(testEmail)).toBe(true);
    expect(constructor.test(testEmail)).toBe(true);
    expect(RegexPatterns.email.test(testEmail)).toBe(true);
  });
});

Custom Test Runner

class RegexTestRunner {
  constructor() {
    this.suites = new Map();
    this.results = {
      totalTests: 0,
      passed: 0,
      failed: 0,
      skipped: 0,
      suites: []
    };
  }
  
  // Add test suite
  suite(name, tests) {
    this.suites.set(name, tests);
    return this;
  }
  
  // Run all test suites
  async run() {
    console.log('Running Regex Tests');
    console.log('='.repeat(50));
    
    for (const [suiteName, tests] of this.suites) {
      const suiteResult = await this.runSuite(suiteName, tests);
      this.results.suites.push(suiteResult);
      
      this.results.totalTests += suiteResult.totalTests;
      this.results.passed += suiteResult.passed;
      this.results.failed += suiteResult.failed;
      this.results.skipped += suiteResult.skipped;
    }
    
    this.printSummary();
    return this.results;
  }
  
  // Run individual test suite
  async runSuite(suiteName, tests) {
    console.log(`\n📋 Suite: ${suiteName}`);
    console.log('-'.repeat(30));
    
    const suiteResult = {
      name: suiteName,
      totalTests: tests.length,
      passed: 0,
      failed: 0,
      skipped: 0,
      tests: [],
      duration: 0
    };
    
    const suiteStart = Date.now();
    
    for (const test of tests) {
      const result = await this.runTest(test);
      suiteResult.tests.push(result);
      
      if (result.status === 'passed') suiteResult.passed++;
      else if (result.status === 'failed') suiteResult.failed++;
      else suiteResult.skipped++;
      
      this.printTestResult(result);
    }
    
    suiteResult.duration = Date.now() - suiteStart;
    
    console.log(`\n📊 Suite Summary: ${suiteResult.passed}/${suiteResult.totalTests} passed`);
    
    return suiteResult;
  }
  
  // Run individual test
  async runTest(test) {
    const result = {
      name: test.name,
      status: 'unknown',
      duration: 0,
      error: null,
      details: {}
    };
    
    if (test.skip) {
      result.status = 'skipped';
      return result;
    }
    
    const start = Date.now();
    
    try {
      if (test.type === 'performance') {
        await this.runPerformanceTest(test, result);
      } else if (test.type === 'security') {
        await this.runSecurityTest(test, result);
      } else {
        await this.runStandardTest(test, result);
      }
      
      result.status = 'passed';
    } catch (error) {
      result.status = 'failed';
      result.error = error.message;
    }
    
    result.duration = Date.now() - start;
    return result;
  }
  
  // Standard validation test
  async runStandardTest(test, result) {
    const { pattern, input, expected, description } = test;
    const regex = new RegExp(pattern);
    
    const actual = regex.test(input);
    
    if (actual !== expected) {
      throw new Error(
        `Expected ${expected}, got ${actual} for input "${input}"`
      );
    }
    
    result.details = { input, expected, actual };
  }
  
  // Performance test
  async runPerformanceTest(test, result) {
    const { pattern, input, maxDuration = 100, iterations = 1000 } = test;
    const regex = new RegExp(pattern);
    
    const start = Date.now();
    
    for (let i = 0; i < iterations; i++) {
      regex.test(input);
    }
    
    const duration = Date.now() - start;
    const avgDuration = duration / iterations;
    
    if (duration > maxDuration) {
      throw new Error(
        `Performance test failed: ${duration}ms (max: ${maxDuration}ms)`
      );
    }
    
    result.details = { duration, avgDuration, iterations };
  }
  
  // Security test (ReDoS detection)
  async runSecurityTest(test, result) {
    const { pattern, maliciousInput, timeout = 1000 } = test;
    
    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        reject(new Error(`ReDoS vulnerability detected: pattern took > ${timeout}ms`));
      }, timeout);
      
      try {
        const regex = new RegExp(pattern);
        const start = Date.now();
        regex.test(maliciousInput);
        const duration = Date.now() - start;
        
        clearTimeout(timer);
        result.details = { duration, maliciousInput };
        resolve();
      } catch (error) {
        clearTimeout(timer);
        reject(error);
      }
    });
  }
  
  // Print test result
  printTestResult(result) {
    const symbols = {
      passed: '✅',
      failed: '❌',
      skipped: '⏭️'
    };
    
    const symbol = symbols[result.status] || '❓';
    const duration = result.duration ? ` (${result.duration}ms)` : '';
    
    console.log(`  ${symbol} ${result.name}${duration}`);
    
    if (result.status === 'failed') {
      console.log(`    Error: ${result.error}`);
    }
    
    if (result.details && Object.keys(result.details).length > 0) {
      if (result.details.input) {
        console.log(`    Input: "${result.details.input}"`);
      }
      if (result.details.avgDuration) {
        console.log(`    Avg per operation: ${result.details.avgDuration.toFixed(4)}ms`);
      }
    }
  }
  
  // Print final summary
  printSummary() {
    console.log('\n📈 Final Results');
    console.log('='.repeat(50));
    console.log(`Total Tests: ${this.results.totalTests}`);
    console.log(`Passed: ${this.results.passed} ✅`);
    console.log(`Failed: ${this.results.failed} ❌`);
    console.log(`Skipped: ${this.results.skipped} ⏭️`);
    
    const successRate = (this.results.passed / this.results.totalTests) * 100;
    console.log(`Success Rate: ${successRate.toFixed(1)}%`);
    
    if (this.results.failed > 0) {
      console.log('\n❌ Failed Tests:');
      this.results.suites.forEach(suite => {
        suite.tests.forEach(test => {
          if (test.status === 'failed') {
            console.log(`  ${suite.name}: ${test.name}`);
            console.log(`    ${test.error}`);
          }
        });
      });
    }
  }
}

// Usage example
const runner = new RegexTestRunner();

// Email validation tests
runner.suite('Email Validation', [
  {
    name: 'Valid email',
    type: 'standard',
    pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
    input: 'user@example.com',
    expected: true
  },
  {
    name: 'Invalid email (no @)',
    type: 'standard',
    pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
    input: 'plainaddress',
    expected: false
  },
  {
    name: 'Performance test',
    type: 'performance',
    pattern: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
    input: 'user@example.com',
    maxDuration: 50,
    iterations: 10000
  },
  {
    name: 'ReDoS security test',
    type: 'security',
    pattern: '^([a-zA-Z0-9._%+-]+)*@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
    maliciousInput: 'a'.repeat(50) + 'X',
    timeout: 100
  }
]);

// Run all tests
runner.run();

Performance Testing and Benchmarking

Comprehensive Benchmarking Framework

class RegexPerformanceTester {
  constructor() {
    this.benchmarks = [];
    this.results = [];
  }
  
  // Add benchmark
  addBenchmark(name, pattern, testData, options = {}) {
    this.benchmarks.push({
      name,
      pattern,
      testData,
      options: {
        iterations: 10000,
        warmupIterations: 1000,
        timeout: 5000,
        ...options
      }
    });
    return this;
  }
  
  // Run all benchmarks
  async runAll() {
    console.log('🚀 Starting Performance Benchmarks');
    console.log('='.repeat(60));
    
    for (const benchmark of this.benchmarks) {
      const result = await this.runBenchmark(benchmark);
      this.results.push(result);
      this.printBenchmarkResult(result);
    }
    
    this.printComparison();
    return this.results;
  }
  
  // Run single benchmark
  async runBenchmark(benchmark) {
    const { name, pattern, testData, options } = benchmark;
    
    console.log(`\n📊 Benchmarking: ${name}`);
    console.log(`Pattern: /${pattern}/`);
    console.log(`Test data: ${testData.length} items`);
    console.log(`Iterations: ${options.iterations}`);
    
    const result = {
      name,
      pattern,
      dataSize: testData.length,
      iterations: options.iterations,
      times: [],
      stats: {},
      errors: []
    };
    
    try {
      // Compile regex
      const regex = new RegExp(pattern);
      
      // Warmup
      console.log('Warming up...');
      await this.warmup(regex, testData, options.warmupIterations);
      
      // Main benchmark
      console.log('Running benchmark...');
      result.times = await this.measurePerformance(
        regex, testData, options.iterations, options.timeout
      );
      
      // Calculate statistics
      result.stats = this.calculateStats(result.times);
      
    } catch (error) {
      result.errors.push(error.message);
    }
    
    return result;
  }
  
  // Warmup phase
  async warmup(regex, testData, iterations) {
    for (let i = 0; i < iterations; i++) {
      testData.forEach(data => regex.test(data));
    }
  }
  
  // Measure performance
  async measurePerformance(regex, testData, iterations, timeout) {
    const times = [];
    const startTime = Date.now();
    
    for (let i = 0; i < iterations; i++) {
      // Check timeout
      if (Date.now() - startTime > timeout) {
        throw new Error(`Benchmark timed out after ${timeout}ms`);
      }
      
      const iterationStart = performance.now();
      
      // Test all data items
      testData.forEach(data => {
        regex.test(data);
      });
      
      const iterationEnd = performance.now();
      times.push(iterationEnd - iterationStart);
      
      // Allow event loop to process
      if (i % 1000 === 0) {
        await new Promise(resolve => setTimeout(resolve, 0));
      }
    }
    
    return times;
  }
  
  // Calculate statistics
  calculateStats(times) {
    const sorted = times.slice().sort((a, b) => a - b);
    const sum = times.reduce((a, b) => a + b, 0);
    
    return {
      total: sum,
      mean: sum / times.length,
      median: sorted[Math.floor(sorted.length / 2)],
      min: Math.min(...times),
      max: Math.max(...times),
      p95: sorted[Math.floor(sorted.length * 0.95)],
      p99: sorted[Math.floor(sorted.length * 0.99)],
      stdDev: this.calculateStandardDeviation(times, sum / times.length),
      opsPerSecond: Math.round(times.length / (sum / 1000))
    };
  }
  
  // Calculate standard deviation
  calculateStandardDeviation(times, mean) {
    const variance = times.reduce((acc, time) => {
      return acc + Math.pow(time - mean, 2);
    }, 0) / times.length;
    
    return Math.sqrt(variance);
  }
  
  // Print benchmark result
  printBenchmarkResult(result) {
    if (result.errors.length > 0) {
      console.log('❌ Benchmark failed:');
      result.errors.forEach(error => console.log(`  ${error}`));
      return;
    }
    
    const { stats } = result;
    
    console.log('\n📈 Results:');
    console.log(`  Operations/sec: ${stats.opsPerSecond.toLocaleString()}`);
    console.log(`  Mean time: ${stats.mean.toFixed(4)}ms`);
    console.log(`  Median time: ${stats.median.toFixed(4)}ms`);
    console.log(`  95th percentile: ${stats.p95.toFixed(4)}ms`);
    console.log(`  99th percentile: ${stats.p99.toFixed(4)}ms`);
    console.log(`  Min time: ${stats.min.toFixed(4)}ms`);
    console.log(`  Max time: ${stats.max.toFixed(4)}ms`);
    console.log(`  Std deviation: ${stats.stdDev.toFixed(4)}ms`);
  }
  
  // Print comparison of all benchmarks
  printComparison() {
    const successful = this.results.filter(r => r.errors.length === 0);
    
    if (successful.length < 2) {
      console.log('\n⚠️ Need at least 2 successful benchmarks for comparison');
      return;
    }
    
    console.log('\n🏆 Performance Comparison');
    console.log('='.repeat(80));
    
    // Sort by operations per second (descending)
    const sorted = successful.slice().sort((a, b) => {
      return b.stats.opsPerSecond - a.stats.opsPerSecond;
    });
    
    console.log('Rank | Name                    | Ops/sec      | Mean(ms) | P95(ms)');
    console.log('-'.repeat(80));
    
    sorted.forEach((result, index) => {
      const rank = (index + 1).toString().padEnd(4);
      const name = result.name.substring(0, 23).padEnd(23);
      const ops = result.stats.opsPerSecond.toLocaleString().padStart(12);
      const mean = result.stats.mean.toFixed(2).padStart(8);
      const p95 = result.stats.p95.toFixed(2).padStart(7);
      
      console.log(`${rank} | ${name} | ${ops} | ${mean} | ${p95}`);
    });
    
    // Speed comparison
    const fastest = sorted[0];
    console.log('\n🚀 Speed Comparison (relative to fastest):');
    
    sorted.forEach((result, index) => {
      if (index === 0) {
        console.log(`  ${result.name}: 1.00x (baseline)`);
      } else {
        const ratio = fastest.stats.opsPerSecond / result.stats.opsPerSecond;
        console.log(`  ${result.name}: ${ratio.toFixed(2)}x slower`);
      }
    });
  }
  
  // Memory usage test
  async testMemoryUsage(pattern, testData, duration = 5000) {
    console.log(`\n🧠 Memory Usage Test (${duration}ms)`);
    
    const regex = new RegExp(pattern);
    const initialMemory = process.memoryUsage();
    const startTime = Date.now();
    let iterations = 0;
    
    while (Date.now() - startTime < duration) {
      testData.forEach(data => regex.test(data));
      iterations++;
      
      // Allow GC periodically
      if (iterations % 1000 === 0) {
        if (global.gc) global.gc();
        await new Promise(resolve => setTimeout(resolve, 0));
      }
    }
    
    const finalMemory = process.memoryUsage();
    const memoryDiff = {
      heapUsed: finalMemory.heapUsed - initialMemory.heapUsed,
      heapTotal: finalMemory.heapTotal - initialMemory.heapTotal,
      external: finalMemory.external - initialMemory.external
    };
    
    console.log(`Iterations: ${iterations.toLocaleString()}`);
    console.log(`Heap Used Diff: ${(memoryDiff.heapUsed / 1024 / 1024).toFixed(2)} MB`);
    console.log(`Heap Total Diff: ${(memoryDiff.heapTotal / 1024 / 1024).toFixed(2)} MB`);
    console.log(`External Diff: ${(memoryDiff.external / 1024 / 1024).toFixed(2)} MB`);
    
    return memoryDiff;
  }
}

// Usage example
const perfTester = new RegexPerformanceTester();

// Generate test data
const emailTestData = [
  'user@example.com',
  'invalid.email',
  'test.email+tag@domain.org',
  'plainaddress',
  'user@domain.co.uk',
  '@invalid.com',
  'user@',
  'very.long.email.address@very.long.domain.name.com'
];

// Add benchmarks
perfTester
  .addBenchmark(
    'Simple Email Regex',
    '\\S+@\\S+\\.\\S+',
    emailTestData,
    { iterations: 50000 }
  )
  .addBenchmark(
    'Detailed Email Regex',
    '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
    emailTestData,
    { iterations: 50000 }
  )
  .addBenchmark(
    'RFC Compliant Email',
    '^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$',
    emailTestData,
    { iterations: 20000 }
  );

// Run benchmarks
perfTester.runAll();

Conclusion

Testing and debugging regex patterns is a critical skill for any developer working with regular expressions. Key takeaways from this guide:

Essential Testing Practices

  • Test-Driven Development: Write test cases before implementing patterns
  • Comprehensive Coverage: Test positive cases, negative cases, and edge cases
  • Performance Testing: Ensure patterns perform well with realistic data volumes
  • Security Testing: Check for ReDoS vulnerabilities with malicious inputs
  • Cross-Platform Testing: Verify patterns work across different environments

Debugging Strategies

  • Break Down Complex Patterns: Test individual components separately
  • Visualize Execution: Understand how the regex engine processes your pattern
  • Use Debugging Tools: Leverage regex debuggers and testing frameworks
  • Monitor Common Mistakes: Watch for unescaped characters, ReDoS patterns, and missing anchors

Best Practices

  • Automate Testing: Use testing frameworks for consistent validation
  • Document Test Cases: Maintain clear documentation of what each test validates
  • Regular Monitoring: Set up monitoring for regex performance in production
  • Continuous Improvement: Regularly review and update patterns based on real-world usage

Remember: A well-tested regex pattern is not just correct—it's also secure, performant, and maintainable. Invest time in proper testing, and your regex patterns will serve you reliably in production environments.