The idea

A bug that had no test is a bug that can come back.

That’s the whole thing. Red-green is a simple discipline: when you find a bug, you don’t just fix it. First you reproduce it. Then you write a test that fails because of the bug (that’s the red). Then you fix the bug. Then you watch the same test pass (that’s the green). In that order, every time, and you actually watch each step happen instead of assuming it did.

It sounds obvious written down like that. The interesting part is why the order matters, and why now is the right time to be strict about it.

There’s a specific reason this matters more with LLMs writing code than it ever did with just me. A model tends to repeat the same mistakes. Sometimes the wrong way to do something just looks like the right way, plausible, idiomatic, the kind of thing you’d write without thinking twice. Fix the bug once without a test, and there’s nothing stopping the model from reaching for that same “obviously correct” wrong way again the next time it touches that code. A regression test is what actually stops it: a permanent, checkable “no, not this” that survives long after you’ve forgotten the bug ever happened.

This is not a new idea (1957 - today)

I want to be clear about one thing: I’m not presenting anything new here. Red-green is the current shape of an idea that programmers have been rediscovering for almost seventy years.

The oldest reference I found is from 1957. Daniel McCracken, in Digital Computer Programming, wrote that “it is necessary to have a hand-calculated check case with which to compare the answers which will later be calculated by the machine”. Know the expected answer before the machine runs: that’s the red test, on paper.

In the early 1960s, NASA’s Project Mercury team did it with punched cards. They wrote the test cases before the code, and worked in half-day iterations. Test-first micro-cycles, decades before anyone called it that. And in 1968, the NATO Software Engineering Conference already stated that “testing is interlaced with the designing instead of being used after the design”.

The modern form comes from Kent Beck. He wrote SUnit for Smalltalk in 1994, then ported it to Java with Erich Gamma during a plane trip in 1997: that port is JUnit, and it spawned the whole xUnit family we all use today. The red and green colors themselves come from the JUnit GUI: a green bar when the tests pass, a red bar when one fails. Beck then codified the practice inside Extreme Programming and formalized the red-green-refactor cycle in Test-Driven Development: By Example (2002).

The part I like the most: Beck never claimed he invented it. He says he rediscovered it, from an ancient programming book describing exactly the McCracken workflow: type the output tape you expect, then program until the actual output matches.

So the technique survived punched cards, tapes, Smalltalk, and Java. What I’m describing in this post is just the next environment it adapts to: LLMs.

Motivation

For a long time, red-green was a nice idea I couldn’t afford. Not because I didn’t believe in it. Because it was expensive. Reproducing every single bug, then writing a test for it, then wiring that test into the suite: that takes time. And when the time is mine, I have to pick my battles. I’d fix the bug, tell myself I’d add the test later, and move on. The test rarely happened.

So the discipline was real, but the economics were against me. The most expensive part was never the fix. It was the test.

That’s the part that changed. Now an LLM can write as much code as I need, almost instantly, almost for free. The cost of producing a test dropped to near zero. So the calculation flips: if I hit a bug, I can just ask for the test that reproduces it, and there’s no reason not to.

The one cost that’s left is runtime. A test still has to run, over and over, on every change. So the rule I keep is simple: as long as the test is cheap to execute, it earns its place in the suite. If it’s fast, it stays.

So the motivation isn’t only that the AI era needs this kind of rigor. It’s that the AI era finally makes it cheap. The part that used to stop me: writing the test, is now the free part.

What happens when you skip it

Let me describe the scenario that made me take this seriously.

You’re working on a complex project with Claude Code, or Cursor, or whatever LLM you like. You hit a bug. You fix it. You move on. No test.

Then, a while later, the same bug comes back. Or a close cousin of it. And you fix it again. And it comes back again.

Here’s why this is worse with an LLM than it ever was for me alone. The reason a bug exists is usually that some piece of the code looks right. It reads well. At first glance it makes sense. That’s exactly what fooled you into writing it, and it’s exactly what steers the model into writing it again. The wrong thing is the thing that seems reasonable, so the model keeps reaching for it. You end up fixing the same mistake over and over, because nothing in the codebase tells the model “no, this specific thing is a trap, I already tried it.”

A regression test is that “no”. It’s a permanent note to your future self and to every model that touches this code after you: this exact behavior was wrong once, and if it ever comes back, this test goes red. Without it, the fix lives only in your memory and in one git commit nobody will read.

So red-green isn’t just about today’s bug. It’s about maintainability. Every regression test you leave behind makes the project a little harder for an LLM to break in the future, and takes one old bug off the table for good.

Reproduce it first

Before touching any code, I want to be able to say, in one sentence, the exact input that produces the wrong output. “When I call X with Y, I get Z instead of W.”

If I can’t say that sentence, I haven’t found the bug yet. I have a theory about the bug, which is a very different thing. Theories are cheap, and LLMs are especially good at producing confident theories. So the rule is: no fix on a hypothesis. Keep reading, add logging, run the failing path again, until the sentence exists.

This step alone kills a whole category of bad fixes: the ones that “fix” something that was never the actual problem.

Reproduce it in the running thing

This one I learned the hard way. Reading the source and reproducing the bug in the real, running product are not the same thing.

Static reading lies to you. You can stare at the code and find nothing wrong while the running app is provably broken. The opposite happens too: the code looks broken but the bug you’re chasing is actually a stale binary, a cached artifact, an old container image that doesn’t match the source anymore. “Works in the tests, broken in the app” is very often a deploy problem, not a code problem.

So if the product can run, run it. Look at the logs, the side effects, the process state. Confirm the bug exists in the live thing before theorizing about it. Only when the product genuinely can’t run locally (needs production credentials, real hardware) do I accept working from logs and reports alone, and I say so explicitly.

Red: write the failing test

Now, before the fix, I write a test that asserts the correct behavior. Since the bug is still there, the test must fail. That failing run is the red, and it’s the most valuable run of the whole process.

A few rules I follow here:

  • Write the test at the cheapest tier that actually exercises the bug. Unit before integration, integration before end-to-end. Fast tests are the ones that survive in a suite.
  • Name the test after the bug, not after the function. test_retry_does_not_drop_last_chunk tells a story. test_retry_2 tells nothing.
  • Assert the behavior, not the implementation. The test should still make sense after someone refactors the internals.
  • Predict the failure before running it. I write down what assertion I expect to fail and with what kind of message, and then I run it and compare.

That last one matters more than it looks. A test can fail for the wrong reason: a typo, a missing import, a broken setup. A compile error is not a red. A setup crash is not a red. If the failure doesn’t match the prediction, the test isn’t reproducing the bug, it’s just broken, and a broken test proves nothing.

Fix the bug

With a red test in place, the fix becomes the easy part. Change the product code, run the test, repeat until it goes green.

Two rules, and they’re absolute:

  • Never edit the test to make it pass. The moment you touch the test during the fix, you’re negotiating with the evidence. If the test was wrong, that’s a new red step, not an adjustment.
  • Prefer the minimal fix that addresses the root cause over a bigger one that suppresses the symptom. If you find yourself adding a special case that makes the test pass without explaining why the bug happened, you’re hiding the bug, not fixing it.

Green: watch it pass

Two runs close the loop:

  1. The red test now passes. Observed, not assumed. I actually run it and see it green.
  2. The full suite for the affected module still passes. The fix broke nothing else.

Then the test and the fix go into the project together. That’s what turns the test into a regression guard: from now on, anyone (human or model) who reintroduces this bug gets an immediate, named, specific failure telling them exactly what they just broke.

Why the order matters

You might ask: why so much ceremony about the order? Why not fix the bug first and add the test after, since the end state is the same?

Because the end state is not the same. A test written after the fix has never been seen failing. You don’t know if it actually catches the bug. Maybe it passes for a reason that has nothing to do with your fix. Maybe it would have passed before the fix too, which means it guards nothing. The only way to know a test can catch the bug is to watch it catch the bug. That’s what the red run is: proof.

This is also exactly why the discipline pairs so well with LLMs. When I ask a model to fix a bug, the failing test is my evidence that the model understood the problem, and the green run is my evidence that it solved it. I don’t have to trust the model’s explanation. I watch the colors change.

Red, then green. In that order, observed, never assumed.

References

The most relevant sources I found while digging into the history:

  • A History of Test-Driven Development, as Told in Quotes - the McCracken (1957), NATO conference (1968), and Dijkstra (1972) quotes, with attribution.
  • You won’t believe how old TDD is - the NASA Project Mercury story: test cases on punched cards, written before the code, in half-day iterations.
  • Test-driven development - Wikipedia’s overview, including Beck’s “rediscovered, not invented” position and the red-green-refactor formalization in his 2002 book.
  • xUnit - the framework lineage: SUnit (Smalltalk, 1994) to JUnit (the 1997 plane trip with Erich Gamma) to everything we use today.
  • Example-Guided, A Brief History - Joshua Kerievsky tracing the whole family of example-first practices.
  • Kent Beck, Test-Driven Development: By Example (Addison-Wesley, 2002) - the book that named the discipline.