Regression testing
Regression testing is re-running tests after a change to catch anything that used to work and has stopped, in parts of the system nobody meant to touch. The page opens with sixteen cases from one shop’s suite — why each is there, the level that proves it and when it runs — then covers what earns a case and what removes one, which subset runs when, flaky tests by the numbers, the cost nobody budgets for, and four regressions where the case was missing.
- It answers
- Did we break the past?
- Entry rule
- A reason you can name
- Exit rule
- The behaviour is gone
One shop’s regression suite on one screen — sixteen cases, why each is there, and when it runs
Nobody opens a page about regression testing for the definition. They open it because a bug came back, or because the suite now takes forty minutes and people merge without waiting. Below are sixteen cases from the suite of the online shop whose smoke gate the smoke testis this build worth testing?A small, fixed set of checks run right after a build is deployed, answering one question: is this build worth anyone else’s time? It is a gate, not coverage — it proves the build is testable, never that it is correct.Full article →All terms page lists. Rows 1–4 came from bugs that reached customers, 5–8 from incident reviews, 9–12 from promises the shop has made, and 13–16 from the paths money takes. Each row names the lowest level that can prove it and the tier it runs in. Later sections cite the rows by number.
| # | The case protects | Why it is here | Level | Runs |
|---|---|---|---|---|
| 1 | Loyalty points are rounded by their own rule | Bug 2214, March 2026: an invoice-rounding change moved a shared helper, and points paid out a few cents short for nine days | Unit | Every push |
| 2 | A discount code applies once, even after the basket changes | Bug 2291, April 2026: changing a quantity re-applied SPRING10 on top of itself | Unit | Every push |
| 3 | Sign-in survives a page reload in Safari | Bug 2417, June 2026: a cookie attribute change signed every Safari customer out on reload | E2E, journey 2, in WebKit | Every merge |
| 4 | A surname with an apostrophe saves | Bug 2503, July 2026: “O’Neill” failed the address API’s validation, and those customers could not check out | API | Every push |
| 5 | A schema migration does not leave search empty | Incident 14, May 2026: the reindex job ran before the migration finished, and search returned nothing for 40 minutes. Smoke row 5 catches it after a deploy; this case catches it before the merge | Integration | Every merge |
| 6 | A payment webhook delivered twice makes one order | Incident 17, July 2026: the payment provider retried a webhook, and 212 orders were shipped twice | Integration | Every merge |
| 7 | A warehouse export over 1,000 lines is split, not cut off | Incident 9, Black Friday 2025: the file stopped at line 1,000, and 340 orders were never picked | Integration, builds 1,200 orders | Nightly |
| 8 | Order emails still send when the email provider is slow | Incident 21, September 2026: a 10-second timeout with no retry lost 1,900 confirmations in an hour | Integration, the stub answers in 12 s | Every merge |
| 9 | The partner API keeps its published shape | Promise: the marketplace partner builds on GET /orders/{id} as the shop’s OpenAPI document publishes it | Contract | Every push |
| 10 | A return is accepted on day 30 after delivery and refused on day 31 | Promise: the 30-day returns policy. Days 29, 30 and 31 are the boundary cases, as on Testing LLM products | Unit | Every push |
| 11 | The order assistant never starts a return on a final-sale item | Promise: the same policy, said by the assistant. One of the protected cases in Prompt regression’s gate | Evaluation case, protected | Every prompt or model change, and nightly |
| 12 | Prices include VAT, and the receipt itemises it | Promise to the regulator: the price shown is the price paid | Unit, and one check in E2E journey 1 | Every push |
| 13 | An order total is lines − discount + delivery, in whole cents | Money: never in floating point; each run generates 1,000 random baskets and checks the sum | Unit, property-based | Every push |
| 14 | A full refund returns exactly what was paid, delivery included | Money: the refund goes to the original card, through the payment provider’s sandbox | Integration | Every merge |
| 15 | Two checkouts spending one gift card at once cannot overdraw it | Money: a race found in code review, never in production. Kept because a gift card is cash | Integration, 50 parallel attempts | Nightly |
| 16 | Search → basket → pay → the warehouse has the order | Money: the path every euro takes. Smoke row 9 asks whether it completes; this case asks whether every amount and line is right | E2E, journey 1 | Every merge |
| Tier | Runs on | What runs | Cases | Time | A red result |
|---|---|---|---|---|---|
| Fast | Every push to a pull request | Unit, API and contract | 2,450 | 4 min | The pull request cannot merge |
| Merge | Every merge, in the merge queue | The fast tier again, plus 96 integration cases and the 12 E2E journeys | 2,558 | 11 min | The change does not land on main |
| Full | Nightly, and before every release | Everything, plus 38 slow cases (rows 7 and 15 among them) and the journeys in three browsers | 2,596 | 46 min | No release until a named person decides |
A regression is a broken promise in a place nobody meant to touch
The ISTQBInternational Software Testing Qualifications Board — testing certification bodyThe international body behind the most widely taken software-testing certifications, which also publishes a glossary of testing terms. Its definitions are a common reference in interviews and contracts; they fix vocabulary, not how a team has to work.All terms glossary defines regression testing as change-related testing “to detect whether defects have been introduced or uncovered in unchanged areas of the software”. Two words in that sentence carry the meaning. “Unchanged” says where to look: not at the feature you edited, which its own tests already cover, but at everything around it. And “uncovered” admits something teams forget. A change can expose a defect that was already there, sitting quietly behind behaviour that has now moved.
Row 1 carries the whole mechanism. A developer changed how invoices round, and did it correctly. The loyalty points calculation used the same helper, and nobody editing invoices had reason to know that. Regressions come from coupling you cannot see from the code you are editing: a shared function, a shared table, a shared cache, a configuration value two features read for different reasons, an ordering nobody wrote down. Nobody was careless. The reach of a change is simply not visible from inside it, and it grows with every year the system stays alive.
The diagram is the reason this page is sceptical of clever test selection. A change reaches other features along two kinds of path. Along code paths — one function calling another — a tool can follow it and pick the tests that run that code. Along data and configuration paths — one feature writing a table that a monthly job reads later — no code connects the two, so nothing selects the job’s tests. Those are exactly the paths regressions take most often, which is why the full tier exists.
What earns a permanent case — and the one thing that removes it
A suite without an entry rule grows by taste, and taste has no upper bound. The rule that works is narrow: a case earns permanence when the behaviour has already shown that it matters. Four things show that, and they are the four groups of the table at the top. “Someone thought it would be thorough” is not among them.
A bug that reached a customer — rows 1–4
The strongest source there is. It has been shown that this can break, that nothing caught it, and that someone outside the team paid for it.
An incident review — rows 5–8
The review names a condition nobody was checking. Turning that sentence into a case is the part of a review that survives the quarter.
A promise — rows 9–12
A published API shape, a returns policy, a regulator’s rule. The promise exists whether or not a test does, so make the test exist.
A path money takes — rows 13–16
Checkout, refunds, gift cards. Failures here are not reported by customers filing tickets; they are found by an accountant three weeks later.
Where a case lives matters as much as whether it exists. Martin Fowler’s advice on the test pyramidmore small tests than big onesA model of a test suite with many fast, narrow tests at the bottom, fewer that test services in the middle, and few end-to-end tests at the top. It is an argument about cost, not a quota: the top layer is small because each test there is the slowest and most expensive one a team owns.Full article →All terms is to replicate a bug exposed by a high-level test with a unit test before fixing it: “Then the unit test ensures the bug stays dead.” The shop does exactly that. Row 1 was reported by customers who looked at their points, yet its case is a unit test that runs in milliseconds and names its own cause when it fails. Row 3 is a browser journey only because a Safari cannot be proved anywhere below a real Safari.
The exit rule is stricter, and it is the one teams get wrong. A case leaves the suite when the behaviour leaves the product — the feature was removed, the rule was deliberately changed, the endpoint was retired. Every other reason to delete a case has a better answer, and the table lists the usual ones.
| Why someone wants to delete a case | What to do instead | Delete? |
|---|---|---|
| It is slow | Move it to the full tier, as rows 7 and 15 were, or prove the same behaviour one level lower. | No |
| It fails at random | Quarantine it the same day, with an owner and a seven-day limit; the section on flaky tests says why. | No |
| It is red, and the release is today | Fix the code, or ship with a written, named, dated decision to accept the risk. | No |
| The behaviour changed on purpose | Change the case in the same pull request as the behaviour, so the review sees both. | No — update it |
| The behaviour is gone from the product | Delete it, in the pull request that removes the behaviour. | Yes |
What runs when — the suite outgrows the wait
Every regression suite eventually crosses a line. It gains cases every release, loses them almost never, and one day running all of it on every change takes longer than a developer will wait. People then stop waiting: they merge, start the next task, and read the result after it no longer matters. From that day the interesting question is not “what should we test” but “which subset runs at which moment”, and that is a design decision about risk and feedback speed.
| Selection strategy | How it decides | What it costs you |
|---|---|---|
| Run everything, every time | No decision at all — the whole suite on every change. | Works until the wait exceeds patience; then people merge without reading the result. |
| Tiers by cadence | The shop’s answer: fast tier on every push, merge tier in the merge queue, everything nightly and before release. | A regression in a slow case can hide until the night. The easiest scheme to explain, and the default for most teams. |
| Risk-based selection | A person decides which areas a change threatens and runs those. | Only as good as that person’s model of the system, and it silently degrades as they forget or leave. |
| Test-impact analysis | A tool records which tests run which code, then runs the tests that touch the changed lines. | Real speed-ups on large codebases, and blind to every path in the diagram that is not code. |
| Predictive selection | A model trained on past results picks the tests most likely to fail for this change. Facebook reported halving the cost of testing changes while still reporting over 95% of individual failures and over 99.9% of faulty changes (Machalica et al., 2019). | Worth it at the scale of a monorepo with thousands of changes a day; the missed 5% of failures still needs a full run behind it. |
| Parallelise instead of selecting | Shard the suite across machines, so it finishes in the time of its slowest shard. | Buys years without weakening coverage; costs money and test data that cases do not share. |
A useful default: parallelise first, tier second, and be sceptical of clever selection until the number of changes a day makes it unavoidable. Buying machines is cheaper than an engineer working out why a regression slipped through the smart subset. Whatever you choose, two rules are not negotiable. The merge tier tests each change merged with the current main branch, which is what a merge queue does — the Git branching page explains why two green pull requesta change proposed for reviewA request to merge one branch into another, where the change is reviewed, discussed and checked by the pipeline before it reaches the main branch; GitLab calls the same thing a merge request. Its cost is waiting: a pull request that sits unreviewed for days is where long-lived branches and painful merges come from.Full article →All terms can still break main together. And the full suite runs before every release, because that run is the last moment the memory is complete.
Flaky tests — measured, not mysterious
A flaky test passes and fails on the same code. Google published its numbers in 2016: about 1.5% of all test runs reported a flaky result, almost 16% of tests had some level of flakiness, and about 84% of the transitions from pass to fail that its CI/CDContinuous Integration / Continuous Delivery — automatic build, test and deliveryEvery commit is built and put through the tests automatically (CI), and a build that passes travels to the servers the same way (CD). The point is not automation for its own sake: it is that a breakage shows up in minutes instead of on release day.Full article →All terms system observed involved a flaky test. Read the last figure twice. Five times out of six, the test that has just gone red is one that also fails with no change at all. On an average project of about a thousand tests, 1.5% means some fifteen failures on every release that somebody has to investigate.
The damage is not the wasted investigation. It is what people learn from it. Martin Fowler put it plainly in 2011: once you start ignoring a regression test failure, that test is useless. Worse, the habit spreads: a team that shrugs at the flaky cases soon shrugs at the healthy ones too. Size makes it worse. A year after the first post, Google found that 0.5% of its small tests, 1.6% of medium tests and 14% of large tests had been flaky in a week, and that size predicted flakiness better than the tool a test was written with.
The causes are known, too. Luo and colleagues studied 201 commits that fixed flaky tests in 51 open-source projects and classified the cause of 161. Three causes covered more than three quarters of them, and each has a fix that removes it rather than hiding it.
| Cause | Share of 161 | What it looks like | The fix that removes it |
|---|---|---|---|
| Async wait | 45% | The test checks a result before the work producing it has finished; a fixed sleep is too short on a loaded machine. | Wait for the condition, not for a duration — 54% of these fixes used a wait-for call. |
| Concurrency | 20% | Two threads touch the same object in an order nobody fixed. | No single fix: locks in 31% of cases, deterministic code in 25%. Often a bug in the product, not the test. |
| Test order dependency | 12% | A case passes alone and fails after another one, which left state behind. | Clean the shared state before and after each case — 74% of these fixes did that. |
Two more findings from the same study change how a team should treat a flaky case. 78% of the tests were flaky from the day they were written, so a new case that runs fifty times before it may block anything gives most flaky cases a chance to show themselves before anyone learns to ignore them. And 24% of the fixes changed the code under test, not the test. A quarter of the time, the “flaky test” was a real defect that a test had found and everyone had decided to call flaky.
Quarantine — the right move
- The case leaves the blocking tier the same day it is confirmed flaky.
- It keeps running and reporting, into a separate list with a named owner.
- Limits are written down. Fowler’s examples were eight tests or a week; the shop uses both.
- The suite stays believable, which is the only property that makes a red result mean anything.
Re-run until green — the trap
- Costs nothing today, which is why it becomes the default within a month.
- Teaches everyone that red means “press the button again” rather than “stop”.
- Turns the real defects among flaky cases — a quarter of them in Luo’s study — into green ticks, because a race in the product looks exactly like a flaky test.
- Slows the real signal too: Google noted that a 15-minute test marked flaky needs three failures in a row — 45 minutes — to report a real break.
Test data, upkeep, and the numbers worth watching
A regression suite has a running cost that arrives whether or not anyone planned it. Cases break when the interface is redesigned, when an API adds a field, when test data drifts, when a date rolls over a year boundary. Budget for that upkeep explicitly — a share of every sprint — because the alternative is not a cheaper suite. The alternative is a suite that decays until people ignore it, which costs the same and protects nothing.
The suite is a product with users, and it deserves the same few numbers a product gets. Case count and code coverage are not among them: both grow when cases are added, whether or not the cases protect anything. The table lists the ones that say whether the memory works.
| Number | What it tells you | The shop’s line |
|---|---|---|
| Regressions that reached customers, per release | Whether the suite does its job. The only number that does. | Each one becomes a row, and gets a review |
| Time to a signal, per tier | Whether people will wait for the answer. | Fast under 5 min, merge under 15 |
| Flaky share of runs | Whether a red result is still believed. | Under 0.5% of merge-tier runs |
| Quarantine size and age | Whether quarantine is a repair shop or a graveyard. | At most 8 cases, 7 days |
| Cases with no origin line | How much of the suite nobody can defend. | None among new cases; old ones reviewed when they fail |
Four regressions, and the case that was missing
None of these organisations was short of tests. In each, a change that looked safe — a repair, a routine data update, an edit, a fix for an earlier complaint — broke something that had worked, and in each the case that would have caught it can be named in one sentence.
| Regression | What happened | The case that was missing |
|---|---|---|
| Mars Polar Lander, lost 3 December 1999 | On 4 June 1998 a system test deployed the landing legs with the flight software running. The touchdown sensors had been wired wrongly, so the test showed nothing. The legs were rewired and the sensors checked by hand, but the deployment test itself was not run again. The review board’s most probable cause of the loss: a spurious touchdown signal when the legs deployed, which shut the descent engines down too early. With no telemetry from the descent, it could not rule out others. | The same test, re-run after the repair. The board wrote that a rerun “with the proper wiring in place might have detected the software logic problem”. Checking the fix is not the same as re-running the test that should have caught the bug. |
| Google search, 31 January 2009 | A routine update to the list of sites known to install malware went out with “/” in it, and “/” matches every address. For about 40 minutes per user, every search result carried “This site may harm your computer”, until the on-call engineers reverted the file. | A case on the data: a known-good site is not flagged, run against every list update. No code changed, so no selection by code would have picked it — the dashed paths in the diagram. |
| Apple, “goto fail”, February 2014 | One duplicated line, goto fail;, made iOS and OS X skip the signature check on the server’s key exchange in TLS: a server could present a valid certificate and sign the handshake with any key at all. Apple fixed it in iOS 7.0.6 and 6.1.6 and OS X 10.9.2. Adam Langley, who explained it the next day, wrote that a test case could have caught it, “but it’s difficult because it’s so deep into the handshake”. | A negative case for the promise: a handshake signed with the wrong key must fail. Langley put up exactly that as a test site, on port 1266. The cases nobody writes are the ones where the system must refuse, because the happy path keeps passing. |
| Windows 10 October 2018 Update | After the previous update, users who had moved their Documents or Pictures folders reported an extra empty copy. Microsoft added code to remove those empty folders. Combined with another change to the update sequence, it deleted the original folders with their files still inside. The rollout was paused two days in; data was lost on “one one-hundredth of one percent” of installs. | The stronger promise next to the one being fixed: a cleanup never deletes a folder that still contains files. Microsoft also added a severity field to its feedback tool, to catch high-impact problems “even when feedback volume is low”. |
Where it degrades
The honest limit is in the name. A regression suite remembers; it does not discover. Every case checks something somebody already thought of, so the suite finds fewer new defects every year it runs unchanged, and a green run says only that none of the remembered things broke. New risk needs new cases — from reviews, from exploratory testing, from production signals — and the suite is where they go afterwards, not where they come from.
It degrades into theatre in recognisable ways, and most of them are ways of keeping the green colour while losing the memory.
- A suite that only grows, with no exit rule, so a third of it tests behaviour the product no longer has.
- Regression run as a phase before release rather than on every change, so defects are found at the most expensive moment.
- Every case written as a browser test because that is what the QA team owns, where an API test would prove the same in a hundredth of the time.
- A quarantine list with no owner and no limit — a graveyard with a more optimistic name.
- Automatic retries everywhere, so the pipeline is green and nobody knows how often it was red first.
- The full tier skipped “just this once” before a release, which is the one run whose absence nobody notices until a customer does.
- Measuring the suite by case count or code coverage rather than by regressions that reached customers.
- A fix closed when the reported steps pass, with no case left behind — the bug is gone, and so is the memory of it.
When to use it
Use it when
- In any system that will be changed again — which is every system not being switched off this month.
- Immediately after a bug is fixed: confirm the fix, then keep the case at the lowest level that proves it.
- Around every promise — a published API, a returns policy, a regulator’s rule — before anything has broken.
- Split into tiers by cadence once the full suite is slower than the wait people will actually tolerate.
Avoid it when
- As a phase performed only before a release — a regression found then is found at the most expensive moment.
- Adding a case for every scenario anyone can imagine, which produces a suite too slow to run and too big to keep.
- At the browser level for logic an API or unit test could prove, where the same coverage costs a hundred times more to run and to fix.
- With flaky cases left inside a blocking tier — the first ignored red result is the moment the suite stops working.
Sources and method
What this page stands on
- 12 sources
- 4 documented failures
- Checked October 18, 2026
The failures this page learns from
the regressions in “Four regressions, and the case that was missing”- PostmortemJPL Special Review Board — Report on the Loss of the Mars Polar Lander and Deep Space 2 Missions (JPL D-18709) (opens in a new tab)NASA Jet Propulsion Laboratory · Mar 22, 2000
- PostmortemMarissa Mayer — "This site may harm your computer" on every search result?!?! (opens in a new tab)Official Google Blog · Jan 31, 2009
- ArticleAdam Langley — Apple’s SSL/TLS bug (opens in a new tab)ImperialViolet · Feb 22, 2014
- PostmortemJohn Cable — Updated version of Windows 10 October 2018 Update released to Windows Insiders (opens in a new tab)Windows Experience Blog · Oct 9, 2018
Research
the numbers in “Flaky tests — measured, not mysterious” and the predictive selection row in “What runs when”- ArticleJohn Micco — Flaky Tests at Google and How We Mitigate Them (opens in a new tab)Google Testing Blog · May 27, 2016
- ArticleJeff Listfield — Where do our flaky tests come from? (opens in a new tab)Google Testing Blog · Apr 17, 2017
- ResearchLuo, Hariri, Eloussi, Marinov — An empirical analysis of flaky tests (opens in a new tab)ACM SIGSOFT FSE 2014 · Nov 2014
- ResearchMachalica, Samylkin, Porth, Chandra — Predictive Test Selection (opens in a new tab)IEEE/ACM ICSE-SEIP 2019 · May 2019
Practices and where the terms come from
the definitions, the lowest-level rule in “What earns a permanent case” and the quarantine limits- StandardISTQB Glossary — regression testing (opens in a new tab)ISTQB
- StandardISTQB Glossary — confirmation testing (opens in a new tab)ISTQB
- ArticleMartin Fowler — Eradicating Non-Determinism in Tests (opens in a new tab)martinfowler.com · Apr 14, 2011
- ArticleMartin Fowler — TestPyramid (opens in a new tab)martinfowler.com · May 1, 2012
Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.
Found this useful?
Share it with someone who is working on the same problem.