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
- Testing Fundamentals
- Debugging Techniques and Tools
- Test Case Design and Coverage
- Automated Testing Frameworks
- Performance Testing and Benchmarking
- Security Testing and ReDoS Prevention
- Cross-Platform and Browser Testing
- Maintenance and Monitoring
- 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.