Blog · Guides · 2026-07-16

How to Turn a Bug Into a Regression Test That Sticks

Klavity
TL;DRTo turn a bug into a regression test, reproduce it reliably, write a test that fails on the broken code, apply the fix so the test goes green, and add it to CI. The one rule that makes it real: the test must fail before the fix and pass after — otherwise it proves nothing and guards nothing.

To turn a bug into a regression test, reproduce the bug reliably, write an automated test that fails on the broken code, apply the fix so the test turns green, and add it to your CI pipeline. The one rule that makes this real: the test must fail before the fix and pass after. A test written after the fix that has never seen the bug fail proves nothing — it can pass for the wrong reasons and give you false confidence that the bug is guarded.

Why every serious bug deserves a regression test

A bug you fix without a test is a bug you will fix again. Code changes, someone refactors the area, a merge silently reverts a line — and the same defect ships a second time, usually to the same customer who reported it first. That is what “regression” means: a behavior that used to work (or that you deliberately fixed) quietly breaks again.

The regression test is the memory the codebase keeps of that fix. It encodes, in executable form, the sentence “this specific thing must keep working.” Three things make bug-derived regression tests more valuable than tests written from a spec:

  • They target proven-fragile code. A bug is evidence that this exact path is easy to break. That is precisely where a guard earns its keep.
  • They come with a reproduction for free. The bug report already tells you the inputs, the state, and the expected result. Half the work of writing a test is done.
  • They document intent. Six months later, a well-named regression test explains why a strange-looking line exists, so the next developer does not “clean it up” and reintroduce the bug.

How to turn a bug into a regression test, step by step

  1. Reproduce it deterministically first. Before you write any test, get the bug to fail the same way every time by hand or in a script. If you can only reproduce it “sometimes,” you have not found the root cause yet, and a test built on a flaky repro will itself be flaky. Nail down the exact inputs, state, and environment.
  2. Write the test at the lowest level that still catches it. If the bug lives in a pure function, a unit test is fastest and most stable. If it only appears when several components interact, use an integration test. Reach for a full end-to-end test only when the bug depends on the real browser, routing, or UI. Lower-level tests are faster and less flaky, so prefer them when they can still reproduce the defect.
  3. Assert the behavior, not the fix. Test what the user should observe — the correct total, the saved record, the absent error — not the internal change you made. If you assert on the implementation (“this helper was called once”), the test breaks on every refactor and stops protecting anything. Behavioral assertions survive rewrites.
  4. Confirm it fails for the right reason. Run the new test against the unpatched code. It must fail, and the failure message must describe the actual bug. A test that passes on broken code is worthless; a test that fails with an unrelated error (a typo, a missing import) is lying to you. This red step is the whole point — skip it and you have not verified anything.
  5. Apply the fix and watch it go green. Now make the code change. The test should pass. If it does not, either your fix is incomplete or your test asserts the wrong thing — both are worth knowing before you ship.
  6. Name it after the bug and link the ticket. Give the test a name a stranger can read (refund_amount_stays_positive_after_partial_return, not test_bug_4821) and reference the bug ID in a comment. When it fails in two years, whoever is on call should understand the stakes in ten seconds.
  7. Add it to CI so it runs on every change. A regression test that only lives on your laptop guards nothing. It has to run in the pipeline, on every pull request, so the bug cannot return without turning something red.

What separates a durable regression test from a brittle one

The failure mode of regression suites is not too few tests — it is tests so fragile that people start ignoring or deleting them. A regression test earns permanence by being:

  • Deterministic. Control anything non-repeatable: freeze the clock, seed random generators, and mock external network calls. A test whose result depends on the current time, a live API, or execution order will fail randomly and train the team to shrug at red builds. Flakiness is how a good test dies.
  • Focused. One regression test should defend one behavior. A sprawling test that checks fifteen things fails for fifteen reasons and tells you nothing precise. Small tests localize the problem the moment they break.
  • Independent. It should set up its own state and not rely on another test having run first. Shared state across tests is one of the most common sources of intermittent failures — the same category of problem behind most flaky end-to-end tests.

Where the reproduction data actually comes from

The hardest part of writing a regression test is usually not the assertion — it is recreating the exact state in which the bug appeared. This is where the quality of the original bug report decides how fast you can build the test. A report that says “checkout is broken” leaves you guessing. A report that carries the URL, the console errors, the failing network request, and the browser and viewport hands you the test’s setup on a plate.

That is the practical case for capturing rich evidence at report time rather than reconstructing it later. Tools like Klavity Snap let anyone file a bug from inside the product with the screenshot, console log, network trace, and environment attached, so the person writing the regression test starts from the real state instead of a paraphrase. The better your bug reports are, the cheaper every regression test becomes.

How AI and self-healing tests keep the suite from rotting

End-to-end regression tests decay for a mundane reason: the UI changes. A button gets renamed, a page restructures, and a selector that was catching a real regression now fails because the element moved — not because the bug came back. Teams respond by disabling the test, and the guard is gone.

Self-healing end-to-end tests address this by re-resolving a moved or renamed element from surrounding context instead of failing on a stale selector, so the test keeps verifying the behavior it was written to protect through cosmetic UI churn. That does not excuse you from the discipline above — you still write the failing test first, still assert behavior, still run it in CI. It just lowers the maintenance tax that otherwise erodes a regression suite one deleted test at a time. The goal is a suite where every entry is a bug that can never quietly ship twice.

Key takeaways

  • Write the test before the fix and confirm it fails first
  • Assert the user-visible behavior, not the specific fix
  • Reuse the exact reproduction state from the bug report
  • Add it to CI and link it to the bug ID so it can't rot

FAQ

What is a regression test?

A regression test is an automated check that verifies a specific behavior still works after code changes. A regression test born from a bug asserts the exact behavior that was broken, so if the bug ever returns, the test fails and CI catches it before release.

Should every bug get a regression test?

Not literally every one, but every bug that reached a user, was expensive to diagnose, or lives in critical-path code should. Trivial cosmetic issues in low-traffic areas rarely earn the maintenance cost. Use the rule: if this bug reappearing would embarrass you or page someone, write the test.

Do I write the regression test before or after the fix?

Before. Write the failing test first, confirm it fails for the right reason on the unpatched code, then apply the fix and watch it pass. This 'red-green' order is the only way to prove the test actually catches the bug rather than passing by accident.

How do I keep regression tests from becoming flaky?

Make them deterministic: control time, seed random data, mock external network calls, and assert on observable behavior rather than internal timing. For end-to-end tests, use resilient selectors or self-healing tests so a UI rename doesn't break a test that was catching a real regression.

Catch bugs the moment a human sees them

Klavity: right-click bug reports, AI personas that review your product, and self-healing tests.

Get started free