Regression Testing¶
Regression testing re-runs existing tests to check that a change did not break behaviour that previously worked. It is the reason a codebase can keep changing: without it, the cost and risk of every modification grows until change effectively stops.
graph LR
W[Working behaviour] --> C[Change: feature, fix, refactor, dependency upgrade]
C --> R{Regression suite}
R -->|green| SHIP[Change is safe to proceed]
R -->|red| FIX[Break identified before release]
Where regressions come from¶
| Source | Example |
|---|---|
| Direct change | A fix alters shared logic and changes another caller's behaviour |
| Side effect | A new index changes query ordering that something depended on |
| Dependency upgrade | A library changes a default, such as date parsing or rounding |
| Configuration | An environment variable differs between environments |
| Data | A migration alters values that code assumed |
| Reintroduction | An old bug returns because a fix was reverted or lost in a merge |
The last row is the argument for adding a test with every bug fix. A defect that returns undetected costs the entire investigation a second time.
Selecting what to re-run¶
Running everything on every change is ideal and often impractical past a certain suite size. The selection strategies, in increasing order of cost to set up:
graph TD
A[Change] --> S1["Retest-all: run everything"]
A --> S2["Risk based: run the areas most likely affected"]
A --> S3["Impact analysis: run what the changed code reaches"]
A --> S4["Test impact analysis: tooling maps tests to code, run the mapped subset"]
| Strategy | Needs | Risk |
|---|---|---|
| Retest-all | Nothing, but time | None, other than slow feedback |
| Risk based | Judgement and defect history | Human judgement misses indirect effects |
| Impact analysis | Dependency knowledge and traceability | Indirect coupling is easy to miss |
| Test impact analysis | Coverage-based tooling in the pipeline | Stale mappings after large refactors |
Practical compromise used by most teams: fast tests on every commit, the full suite before merge or nightly, and the full suite always before release.
Keeping the suite from strangling the team¶
A regression suite grows monotonically unless it is managed, and a suite that takes hours gets skipped, which returns the team to having none.
- Push tests down the pyramid. A regression check that a unit test can make should never be an end-to-end test.
- Parallelise and shard. Wall clock time is what determines whether the suite is run.
- Delete duplicates. Many old cases test the same rule through different paths, and deleting them loses nothing.
- Treat flakiness as a defect. One tolerated flaky test teaches the team to re-run until green, which disables the whole suite in practice.
- Prune by value. Tests for removed features and long-obsolete behaviours are pure cost.
Regression testing and refactoring¶
The relationship is worth stating explicitly: refactoring is defined as changing structure without changing behaviour, so it is only meaningful when something can confirm the behaviour did not change. That confirmation is the regression suite.
This is also why tests coupled to implementation detail are so damaging. They break during refactoring even though behaviour is unchanged, which trains the team to treat red as noise.
Check Your Understanding¶
Why should every fixed defect leave an automated test behind?
A regression suite takes four hours and the team has started skipping it before merges. What is the most effective response?