Regression Testing
Purpose
Ensure that new code changes do not break existing functionality through effective test selection, suite design, automation maintenance, and metric-driven optimization. This skill covers impact analysis, risk-based selection, prioritization, flaky test management, and CI integration.
Agent Protocol
Trigger
User mentions regression testing, test selection, regression suite, flaky tests, automation maintenance, or asks what to re-test after a change.
Input Context
- Code changes (commits, PRs, commits since last regression run)
- Existing test suite composition and execution history
- Risk assessment of changed areas
- Time/resource constraints for regression execution
Output Artifact
- Regression test selection recommendation
- Optimized regression suite definition
- Flaky test report and remediation plan
- Regression metrics dashboard or report
Response Format
Structured recommendation with:
- Impact analysis summary (changed areas to affected tests mapping)
- Selected test list with risk-based justification
- Prioritization within the selection (critical path first)
- Risk assessment of excluded tests (what we're accepting risk on)
- Execution time estimate and time budget compliance
Completion Criteria
- Regression test selection produced and justified through risk analysis
- Risk coverage meets agreed threshold (excluded tests documented with rationale)
- Execution time fits within available window via prioritization
- Flaky tests identified, flagged, and quarantined
- Selection recall validated (no regression test gaps identified)
Workflow
- Impact analysis: Map code changes to affected files, then to test suites via dependency graph and coverage data
- Test selection: Choose tests based on risk model (business criticality, failure history, change proximity). Use dependency graph for precision
- Prioritization: Order selected tests by risk score. Apply time budget if constrained — highest risk tests run first
- Execution: Run selected regression suite. Monitor results in real-time. Record execution times
- Flaky detection: Re-run failed tests up to 3 times. If inconsistent, quarantine immediately. Track flakiness rate
- Analysis: Correlate new failures with recent changes. Distinguish real regressions from environmental issues
- Optimization: Remove redundant tests, consolidate overlapping coverage, retire obsolete suites. Update risk scores based on results
- Reporting: Generate pass/fail summary, risk coverage report, execution time trend, and flaky test log
Architecture / Decision Trees
Test Selection Strategy Decision Tree
How many tests are in the suite?
├── < 100 → Run all (no selection needed)
├── 100-1000 → Dependency-graph selection
│ ├── Dependency graph available? → Use graph
│ └── No graph → Coverage-based selection
├── 1000-10000 → Risk-based + dependency selection
│ ├── Historical data available? → Add failure rate to risk model
│ └── No history → Coverage + module risk only
└── 10000+ → ML-based selection
├── Data available? → Train selection model
└── No data → Start with risk-based, collect data
Test Layer Decision Tree
Layer 1 (Smoke): < 5 min, always run on every deployment
Critical path tests, auth, main workflow
Layer 2 (Core): < 30 min, run on every merge
All core business functionality, all integration tests
Layer 3 (Full): < 2 hours, run daily
Complete suite including edge cases
Layer 4 (Extended): 2-8 hours, run pre-release
Full suite + performance + chaos + security
Common Pitfalls
- Suite bloat without pruning: Regression suites accumulate dead tests. Review and remove obsolete tests quarterly
- Running all tests on every change: Doesn't scale. Invest in test selection when suite exceeds 100 tests
- Flaky test accumulation: Tests that fail intermittently erode trust. Quarantine within 24 hours
- No risk model: Treating all tests as equally important misses business-critical risks
- Ignoring test selection recall: Not tracking what was missed means you don't know your blind spots
- Brittle test selection: Hardcoded file lists in selection logic break as codebase evolves
- Manual regression processes: Manual regression is slow, error-prone, and doesn't scale
- No time-budget management: Without time constraints, regression suites grow unbounded
- Coverage over risk: 100% coverage is meaningless if it covers the wrong things
- No feedback loop: Without analyzing missed bugs, regression can't improve
Best Practices
- Layer the regression suite: smoke (fastest), core (balanced), full (slowest), extended (pre-release)
- Base test selection on dependency graphs and historical failure data
- Prioritize tests by risk score: business criticality + failure probability + change proximity
- Quarantine flaky tests immediately and fix within one sprint
- Add regression tests for every production bug fix (prevent re-introduction)
- Regularly audit the regression suite: remove dead tests, consolidate duplicates
- Use time budgets with priority ordering for constrained execution windows
- Track regression metrics: pass rate, execution time, flakiness, risk coverage
- Automate regression selection and execution in CI pipeline
- Create feedback loops: analyze missed bugs and improve test coverage
Compared With
| Aspect | Regression Testing | Smoke Testing | Acceptance Testing |
|---|---|---|---|
| Scope | Existing functionality | Critical path | Business requirements |
| Frequency | Every change + scheduled | Every deployment | Per release/story |
| Suite size | 100-10000+ tests | 5-20 tests | 10-100 scenarios |
| Execution time | Minutes to hours | < 5 minutes | Hours to days |
| Selection | Risk-based + selection | Fixed suite | Per-feature planning |
| Focus | Regression detection | Stability check | Requirement validation |
| Automation | Fully automated | Fully automated | Mixed (automated + manual) |
Performance Considerations
- Test selection can reduce execution time by 60-80% while maintaining 85%+ defect detection
- Dependency graph analysis: < 1 second for most codebases. Pre-compute for large monorepos
- Risk scoring: lightweight (file-level analysis) vs heavy (ML-based scoring for 10000+ tests)
- Parallel execution: N-cores speedup, but consider shared resource contention
- Flaky test re-runs: add 2x execution time for flaky detection. Reduce by quarantining flaky tests
- Time budget: allocate based on pipeline criticality. Commit stage: < 5 min. Merge stage: < 30 min
Regression Test Selection Examples
Dependency Graph-Based Selection
# .regression/dependency-graph.yml
services:
- name: "payment-service"
modules:
- "src/payment/**"
- "src/common/transactions/**"
affected_tests:
- "tests/payment/**"
- "tests/integration/checkout/**"
risk_score: 0.9
business_criticality: "P0"
- name: "user-service"
modules:
- "src/user/**"
affected_tests:
- "tests/user/**"
- "tests/integration/auth/**"
risk_score: 0.7
business_criticality: "P1"
Risk Scoring Matrix
risk_scoring:
factors:
change_proximity:
weight: 0.4
rules:
- "directly modified file: score 1.0"
- "imported by modified file: score 0.5"
- "transitively imported: score 0.2"
failure_history:
weight: 0.3
rules:
- "failed in last 10 runs: score 1.0"
- "failed in last 50 runs: score 0.5"
- "never failed: score 0.1"
business_criticality:
weight: 0.3
rules:
- "P0 (revenue-critical): score 1.0"
- "P1 (important): score 0.7"
- "P2 (standard): score 0.4"
- "P3 (nice-to-have): score 0.1"
thresholds:
run_always: ">= 0.8"
run_if_budget: ">= 0.4"
skip: "< 0.4"
CI Integration for Regression Testing
GitHub Actions — Tiered Regression Pipeline
name: Regression Tests
on:
pull_request:
branches: [main]
schedule:
- cron: "0 2 * * *" # Daily full regression
jobs:
smoke-tier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx playwright test --grep @smoke --timeout 30000
env:
CI: true
- if: failure()
run: echo "Smoke tests failed - blocking merge"
# Pipeline stops here on failure
core-tier:
needs: smoke-tier
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- name: Compute affected tests
run: |
CHANGED=$(git diff --name-only origin/main...HEAD)
node .regression/select-tests.js --changes="$CHANGED" --tier=core
- run: npx vitest run --shard=${{ matrix.shard }}/3 $(cat test-list.txt)
env:
CI: true
full-regression:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- uses: actions/checkout@v4
- run: npx vitest run --shard=${{ matrix.shard }}/8
Regression Testing Anti-Patterns
Anti-Pattern: Running Everything Every Time
Running the complete regression suite on every commit wastes CI resources and slows feedback. Once a suite exceeds 100 tests, implement test selection. Start with dependency-graph-based selection, add risk scoring as historical data accumulates.
Anti-Pattern: No Flaky Test Quarantine
Failing to quarantine flaky tests erodes trust in the entire test suite. Developers start ignoring failures, including real regressions. Quarantine flaky tests within 24 hours. Fix within one sprint or delete the test.
Anti-Pattern: Manual Regression Execution
Running regression tests manually is slow, error-prone, and doesn't scale. Every regression test must be automated. Manual testing has its place (exploratory, UAT) but regression is repetitive by definition.
Anti-Pattern: Letting Suite Bloat
Regression suites grow without bound as new features are added without removing obsolete tests. A 10,000-test suite that's never pruned has significant dead weight. Audit quarterly: remove tests for removed features, consolidate overlapping tests, retire flaky tests.
Anti-Pattern: No Time Budget Management
Without time budgets, regression suites grow unbounded. Each sprint adds tests but never removes them. Set a total suite time budget. When the budget is exceeded, prioritize: run highest risk tests first, defer low-risk tests to nightly.
Anti-Pattern: Ignoring Test Selection Recall
Using test selection without measuring recall means you don't know what you're missing (false negatives). Track bugs that escaped to production and should have been caught by regression. If recall drops below 75%, refine the selection model.
Regression Testing Maturity Model
| Level | Characteristics | Practices |
|---|---|---|
| 1: Initial | Manual regression | Testers manually re-test critical paths, no automation, no tracking |
| 2: Defined | Automated regression suite | Full suite automated, run on every merge, basic CI integration |
| 3: Managed | Tiered regression | Smoke/core/full tiers, risk-based selection, test selection by dependency graph |
| 4: Measured | Optimized regression | ML-based selection, flaky test auto-quarantine, regression metrics dashboard, < 60 min full suite |
| 5: Optimized | Predictive regression | AI-driven test selection, mutation-based selection validation, automatic test generation from production failures |
Regression Metrics Dashboard
metrics:
suite_health:
total_tests: 4250
active: 4120
quarantined: 45
flaky_rate: 1.1%
execution_time_avg: "42 minutes"
selection:
avg_tests_per_pr: 340
selection_recall: 87%
false_negative_rate: 2.3%
missed_bugs_last_quarter: 3
quality_gates:
smoke_pass_rate: 99.8%
core_pass_rate: 97.5%
full_pass_rate: 96.2%
blocking_failures_per_week: 2.1
Flaky Test Management
flaky_test_workflow:
detection:
- "Auto-detect: test fails in CI but passes re-run (max 3 retries)"
- "Mark as flaky: quarantine from main suite, notify owner"
- "Track: flaky rate per test, per suite, per team"
quarantine:
- "Move to quarantined suite: excluded from CI gates"
- "Assign owner: last modifier of test file"
- "Set SLA: fix within 5 business days or delete"
investigation:
steps:
- "Check test isolation: shared state, timing dependencies"
- "Check environment: resource contention, network latency"
- "Check data: non-deterministic fixtures, stale mocks"
- "Check assertions: race conditions, async timing"
resolution:
- "Fix root cause: improve isolation, add retry logic, stabilize data"
- "Verify: 10 consecutive runs in CI with zero failures"
- "Re-integrate: move from quarantined back to main suite"
metrics:
flaky_rate_target: "< 1% of total suite"
quarantine_age_target: "< 5 days"
auto_fix_rate: "> 50% (common patterns: wait strategies, data isolation)"
Regression Test Data Strategy
test_data_principles:
isolation:
- "Each test run gets its own dataset — no sharing across runs"
- "Use unique identifiers (timestamp + run ID) to avoid collisions"
- "Clean up after test: delete-by-run-id in afterAll"
factories:
- "Use factory functions with sensible defaults and overrides"
- "Generate unique values for constrained fields (email, username)"
- "Never hardcode IDs — they may not exist in test environment"
seeding:
- "API-level seeding: POST /api/test/seed with required entities"
- "Database-level seeding: SQL INSERT for bulk data needs"
- "Seed in beforeAll, clean in afterAll"
environments:
- "CI: fresh seed per run, cleanup guaranteed"
- "Staging: seeded data with known state, refreshed daily"
- "Production: never use in regression tests except read-only"
Regression Testing Anti-Patterns (Additional)
Anti-Pattern: No Post-Release Validation
Running regression tests only before release and not monitoring production after deployment. The pre-release suite may pass while production has problems due to configuration differences, data volume, or real user behavior. Implement production health checks and canary analysis alongside pre-release regression.
Anti-Pattern: Skipping Security Regression
Security regression tests are skipped to save time in the deployment pipeline. Security vulnerabilities re-emerge when changes inadvertently reintroduce previously fixed issues. Security regression tests must run on every deployment regardless of change scope.
Anti-Pattern: Manual Regression Sign-Off
Relying on manual testing for regression sign-off. Manual regression is slow, error-prone, and doesn't scale beyond a few tests. Every regression test must be automated. Reserve manual testing for exploratory testing and UAT.
Rules
- Every change must have at least a smoke test pass before regression is considered
- Do NOT run full regression on every commit — select based on risk assessment and time budget
- Flaky tests must be quarantined within 24 hours of detection — no false signals in main suite
- Regression suite must complete within the available deployment window. Use prioritization if constrained
- Test selection decisions must be documented with risk rationale — especially excluded tests
- Coverage gaps identified during regression must be tracked and resolved within one sprint
- Every production bug fix must include a regression test that reproduces the bug
- Regression suite must be audited quarterly: remove dead tests, consolidate duplicates
- Risk-based selection recall must be measured and maintained above 75%
- Regression test execution time trend must be monitored — any increase > 20% requires investigation
- Only deterministic tests belong in the regression suite — no tests with random or time-dependent behavior
- Regression test failures must block deployment until root cause is identified and fixed
- Obsolete tests (testing removed functionality) must be removed within one sprint
- New regression tests must pass 10 consecutive runs before being added to the suite
- Security regression tests must run even for unrelated changes (defense in depth)
- Regression test selection must include all tests for security-critical modules regardless of change scope
- Each regression tier must have a defined time budget and exit criteria
- False negative rate must be tracked and maintained below 5%
- Test data must be isolated per run — never share test data across regression runs
- Flaky tests are quarantined within 24 hours, fixed within 5 days, or deleted
References
- references/automation-maintenance.md — Test Automation Maintenance
- references/flaky-test-management.md — Flaky Test Management
- references/regression-metrics.md — Regression Metrics
- references/regression-suites.md — Regression Suite Design
- references/regression-testing-advanced.md — Regression Testing Advanced Topics
- references/regression-testing-architecture.md — Regression Testing Architecture and System Design
- references/regression-testing-fundamentals.md — Regression Testing Fundamentals
- references/regression-testing-strategy.md — Regression Testing Strategy and Decision Frameworks
- references/risk-based-selection.md — Risk-Based Test Selection
- references/test-selection.md — Regression Test Selection
Handoff
After regression testing, hand off to:
quality-smoke-testing— for BVT smoke suite updates on new critical pathsquality-acceptance-testing— if acceptance criteria gaps were uncoveredquality-e2e-testing— for end-to-end coverage of newly identified risk areasquality-integration-testing— for deeper investigation of regression failures
Implementation Patterns
Regression Test Suite Runner
from typing import List, Dict, Optional, Callable
from dataclasses import dataclass
from datetime import datetime
import json
import time
@dataclass
class RegressionTest:
name: str
run_fn: Callable
tier: int # 1=critical, 2=important, 3=nice-to-have
module: str
timeout: int = 30
retries: int = 0
@dataclass
class TestResult:
name: str
passed: bool
duration_ms: float
error: Optional[str] = None
retry_count: int = 0
class RegressionSuite:
def __init__(self, name: str):
self.name = name
self.tests: List[RegressionTest] = []
def add_test(self, test: RegressionTest):
self.tests.append(test)
def run_tier(self, tier: int, parallel: bool = False) -> Dict:
tests = [t for t in self.tests if t.tier == tier]
results = []
start = time.time()
if parallel:
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._run_single, t): t for t in tests}
for future in concurrent.futures.as_completed(futures):
results.append(future.result())
else:
for test in tests:
results.append(self._run_single(test))
duration = time.time() - start
passed = [r for r in results if r.passed]
failed = [r for r in results if not r.passed]
return {
"suite": self.name,
"tier": tier,
"total": len(results),
"passed": len(passed),
"failed": len(failed),
"duration_seconds": round(duration, 2),
"results": results,
}
def _run_single(self, test: RegressionTest) -> TestResult:
for attempt in range(test.retries + 1):
start = time.time()
try:
test.run_fn()
duration = (time.time() - start) * 1000
return TestResult(
name=test.name,
passed=True,
duration_ms=round(duration, 2),
retry_count=attempt,
)
except Exception as e:
if attempt < test.retries:
continue
duration = (time.time() - start) * 1000
return TestResult(
name=test.name,
passed=False,
duration_ms=round(duration, 2),
error=str(e),
retry_count=attempt,
)
return TestResult(name=test.name, passed=False, duration_ms=0)
def run_all(self, parallel: bool = False) -> Dict:
tiers = sorted(set(t.tier for t in self.tests))
all_results = {}
for tier in tiers:
all_results[tier] = self.run_tier(tier, parallel)
return all_results
class FlakyTestDetector:
def __init__(self, window_size: int = 10):
self.history: Dict[str, List[bool]] = {}
self.window_size = window_size
def record_result(self, test_name: str, passed: bool):
if test_name not in self.history:
self.history[test_name] = []
self.history[test_name].append(passed)
if len(self.history[test_name]) > self.window_size:
self.history[test_name].pop(0)
def is_flaky(self, test_name: str, threshold: float = 0.2) -> bool:
if test_name not in self.history:
return False
results = self.history[test_name]
if len(results) < 5:
return False
pass_rate = sum(results) / len(results)
return pass_rate < (1 - threshold) and pass_rate > threshold
Architecture Decision Trees
Regression Test Scope Selection
What changed?
├── New feature added
│ ├── Add tests for new feature
│ ├── Run full regression for affected module
│ └── Run smoke tests for entire system
│
├── Bug fix
│ ├── Add regression test for the fixed bug
│ ├── Run all tests in the affected module
│ └── Run integration tests for dependent modules
│
├── Refactoring (no behavior change)
│ ├── Run full test suite for refactored module
│ ├── Contract tests for API changes
│ └── Compare before/after test coverage
│
├── Dependency update
│ ├── Run full regression suite
│ ├── Run security-focused tests
│ └── Test edge cases in updated dependency
│
└── Configuration / infrastructure change
├── Smoke tests for deployment
├── Health check tests
└── Performance benchmark comparison
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| No tiered regression | Full suite too slow for CI feedback | Tier 1 (critical) in 5 min, Tier 2 in 15 min, Tier 3 nightly |
| Ignoring flaky tests | Wasted CI time, distrust in suite | Quarantine flaky tests immediately, fix within 5 days |
| Only adding, never removing | Suite grows unbounded, slow | Remove tests for deleted features, review suite quarterly |
| Determinism not enforced | Random failures from shared state | Isolate test data per run, no shared state |
| No baseline for comparison | Don't know if performance regressed | Store baseline metrics, compare on each run |
Performance Optimization
- Impact analysis for test selection: Use code coverage data to select tests for changed files. Only run tests that cover modified code. Reduces regression run time by 60-80%.
- Parallel test execution by tier: Run Tier 1 tests sequentially for fast feedback. Run Tier 2+3 tests in parallel across multiple machines. Total regression time: Tier 1 in 5 min, all tiers in 30 min.
- Test impact analysis: Use
git diffto identify changed files. Map files to test suites. Only run affected suites. Cache unaffected suite results from previous CI run.