0

Red-Green-Refactor

Green stage says, Make the test work quickly, committing whatever sins necessary in process

Having an idea of writing a test & make it work quickly, does not look intuitive for a good software engineering approach.

Refactor stage is the actual stage, where we do real programming, after performing sins necessary to clear Red stage, which looks crazy.

For a given requirement, a simple solution design that make us think about creating good abstractions and then Implement with testing looks very intuitive.


Is TDD a brain wash? from Kent beck... Why would a good Software engineer, make the test work quickly, committing whatever sins necessary in process?

Why would the refactor stage be applied after completing a dumb green stage? Are those test cases(in Red stage) missing in non-TDD approach?

2
  • 1
    Are any of the above quotes? If so, please format them as such.
    – l0b0
    Commented Jun 15, 2020 at 4:57
  • 1
    … and provide a source, including context. Commented Jun 15, 2020 at 4:59

6 Answers 6

7

1) You Ain't Perfect

Even if you had a great upfront design handed to you by some sacred initiate of the divine architectural order. You are probably going to make a mistake.

Now you could write out all of the code upfront, exactly to specification, and then press the compile and run button.

  • But it probably won't compile.
  • You have probably made 3-4 mistakes per hundred lines of code, more if you rushed it, less if you have already manually cross-checked it.
  • You are going to spend days, weeks, even years debugging this code.

The upside is that you don't have to tidy up much rubbish (assuming the upfront design had very little: garbage in -> garbage out). It just happens to be distributed over a lot of ground.

The alternative is to slice the requirements into smaller pieces throwing up makeshift abstractions (that are sinfully hard coded and imperfect right now), to work on the small piece of code until it works.

  • It compiles
  • You've probably only made 3-4 mistakes
  • Debugging takes minutes, maybe an hour if you really aren't familiar with the logic.

The downside is you have a lot of shoring, framing, and rubbish around a small nucleus of pretty and clean code.

2) You are Not an Acolyte of the Divine Order of Architecture, nor are You their Friend

Excluding a few kinds of very well researched problem (I'm looking at sorting... maybe), chances are any upfront plan is going to have at least one box, or cloud labelled: does something, somehow.

Which is a terribly specific definition. You are probably going to have to explore this space.

You could come along with a Perfect Plan™, based on the definition of somehow. Chances are you going to do something stupid, not because you are stupid, but because you didn't bother to learn the actual landscape.

You could instead find a path that allows you to fulfil one requirement of the somehow, with a lot of dodgy workarounds, and todos - all of those "sins". Each of those "sins" represents a lack of understanding. Sometimes though you will discover that the path already found isn't quite good enough, and that you will need to find another path. No one said that path finding was easy.

TDD

... is the distillation of those two pieces of information.

  • Be practical, you are going to make mistakes. Reduce the change surface for introducing mistakes, to make them as obvious as possible.

  • Be humble, you don't known what you don't know. Understand that every kludge you use is a lack of understanding. And that is okay. Write the piece you do understand and get it working while placing those workarounds and supports. Just remember to go back to those workarounds and supports, investigate the problem they are trying to conceal and work it out. It may work out, it may reveal that you have to discard a lot of work and start again.

The most important part though is to realise that TDD is one tool. Don't use it when another tool will work better for you.

4

Sometimes just implementing a feature can be tricky enough, and if you can't even get it working the "dumb" way, then trying to make it look good is pointless. This is often quoted as "make it work, make it right, make it fast".

The "green" step lets you know when you're done writing the minimum code required to make a feature work, and ensures you don't get carried away trying to think of the best possible design first. In fact, it is not always clear what the "best design" is upfront, and writing "dumb" code first can help identify what needs to be improved (this is true whether or not you follow TDD). To quote the refactoring article on the XP website:

Let go of your notions of what the system should or should not be and try to see the the new design as it emerges before you.

5
  • "Sometimes just implementing a feature can be tricky enough" Firstly, this situation does not arise, because a project starts by having right people on board. Clarity on design idea behind implementation is generally clear before solution is implemented Commented Jun 15, 2020 at 3:46
  • 7
    @overexchange I don't know what kind of projects you were on, but you are definitely wrong in this statement. On projects I've worked on, there were many times, where unforseen and unknown circumstances and designs forced us to completely re-think our 'perfect solution' we created up-front. If we tried to create simplest solution first, we wouldn't need to waste time and effort creating 'perfect' designs up-fron that would then need to be scrapped.
    – Euphoric
    Commented Jun 15, 2020 at 4:54
  • @overexchange: Sometimes, question come up during development, such as (just a random example) whether to use json.Marshal or Encoder.Encode. Not everybody knows the answer to this beforehand, sometimes you just choose one to make it work, then change your decision later. Commented Jun 15, 2020 at 5:00
  • "Sometimes", "not always", "can help"... I fully agree that all of those qualified statements are true, but is that sufficient to justify a philosophy that is often promoted as the only and exceptionless way to do things? Commented Jun 15, 2020 at 5:34
  • 2
    because a project starts by having the right people on board the project starts having the people you have, not the one you wish. Companies don't set up "dream teams". Sometimes you don't even have enough ppl working on the project. Sometimes they don't even know in deep the stack...
    – Laiv
    Commented Jun 15, 2020 at 10:10
4

For a given requirement, a simple solution design that make us think about creating good abstractions and then Implement with testing looks very intuitive.

You're right that a good abstraction is very important. So important that before you settle on an abstraction it's worth writing code that uses that abstraction and seeing what it will look like.

Oh wait, that's what we do in the red stage anyway. Tests use the abstraction. Sometimes before it even exists.

Refactoring is meant to happen behind a stable abstraction. If you mess with the abstraction you're not refactoring, you're rewriting. Both your code and your tests. So there is some value to getting the abstraction right early.

Now sometimes you can't see the right abstraction at first, so you plod along and make a mess until you do see it. That's fine. You just have to be willing to junk the old stuff and start over. If you do this, be sure it's worth it.

I do this myself. So often I keep a junk folder to dump dead code in. Code that didn't even make it to source control. It's an old habit. Gets me out of corners I've painted myself into. It lets me think with my fingers.

If you want to work this way the big thing to realize is that if you're going to junk code then the sooner you do it the better. The more knowledge of that code spreads, the harder it becomes to remove.

Is TDD a brain wash?

I was doing this before TDD existed. When I learned TDD I saw no reason to stop. I don't see a brain wash here unless you thought TDD was some silver bullet that solved all problems.

4
  • Am sorry.....Abstractions doesn't come on the fly... It is a skill to refine the code and create good abstractions. How can you imagine those abstractions and start writing tests? Commented Jun 15, 2020 at 14:50
  • @overexchange by writing the using code first. Sure it won’t compile but it reads just fine. You can call it “on the fly” it you like but it’s a development style that helps you focus on hiding details and showing intent. Commented Jun 15, 2020 at 14:54
  • Its a complete de-focus... I mean, let me stabilise by designing my abstractions first. Am being distracted wth these test cases for every code change Commented Jun 15, 2020 at 14:56
  • @overexchange You’re mixing together two different activities. Code changes come later. Commented Jun 15, 2020 at 15:09
2

Make the test work quickly, committing whatever sins necessary in process

The word "necessary" is key here. You obviously shouldn't make any of the existing tests fail unless that is necessary to make this test succeed. It is common for a change to make it necessary to also modify other tests to make them all pass. This is completely normal. You also shouldn't add any unnecessary (unreachable, irrelevant or otherwise redundant) code to make it pass.

Refactor stage is the actual stage, where we do real programming

No, the functional change is done in the green stage. That is the programming which must happen to make a functional piece of software. The refactoring is to make a maintainable piece of software.

2

Is TDD a brain wash?

Maybe. I think you can make a case that discource during the Look, Ma, No Hands era was dominated by vagueness and faith.

But Red/Green/Refactor itself has held up pretty well.

Why would a good Software engineer, make the test work quickly, committing whatever sins necessary in process?

Because the GREEN task is about test calibration; verifying that our new automated mistake detector is actually measuring the code that we intend it to be measuring.

The "sinful" part is a matter of focus. Ward Cunningham explained it rather well:

let's focus on the goal. The goal right now is to make this routine do this thing. Let's not worry about what somebody reading the code tomorrow is going to think. Let's not worry about whether it's efficient. Let's not even worry about whether it will work. Let's just write the simplest thing that could possibly work.

Here, the goal is to demonstrate that the automated mistake detector can distinguish between correct behavior and incorrect behavior. Other considerations are deferred until after that goal is met.

Why would the refactor stage be applied after completing a dumb green stage?

Because, above and beyond code that tells the machine what to do, we have additional targets in our design.

Design is what we do, when we want to get more of what we want than we'd get by "just doing it." -- Ruth Malan

Another way to think about this is that we are trying to reduce the technical debt:

if we failed to make our program align with what we then understood to be the proper way to think about our financial objects, then we were gonna continually stumble over that disagreement and that would slow us down which was like paying interest on a loan.

The tests are passing, so we know that the machine understands what we want; we're now reaching toward the other important goal, which is making sure that the human beings who will be maintaining the code understand what we want.

Are those test cases(in Red stage) missing in non-TDD approach?

Again, maybe. In theory, nothing prevents you from introducing all of the tests you want after the production code is done.

In practice: you aren't going to go back and fill in test gaps if you think the production code is "done" and you are deadline constrained. Furthermore, you've missed the opportunity of having the tests inform the design (but maybe you don't need that because your designs are always "testable"), and you've missed the benefits that the mistake detectors offer you while you were developing the production code (maybe you don't need that because you don't make mistakes).

1
  • Answer by Kain0_0 focuses on the conclusion of this answer: the testability of design and fallibility of developers.
    – andrybak
    Commented Jun 19, 2020 at 0:34
0

Disclaimer

I just want to take the time here to point out that your argument essentially boils down to the distinction between waterfall (design before implementation before testing) and agile (test-driven, quick and dirty, continual refactoring).

Neither of these is universally superior to the other. Each approach has its own drawbacks. This answer is not trying to tell you that you have to follow TDD principles, it's only trying to highlight the benefits of doing so, since your question seems to be rooted in not understanding the benefits of the TDD way of doing things.


It didn't say to never improve your code

Having an idea of writing a test & make it work quickly, does not look intuitive for a good software engineering approach.

You're likely not interpreting this the way it was intended.

"writing a test & make it work quickly and then never touch or improve it again" is a bad software engineering approach. But the resource you're quoting never said that you shouldn't refactor, it just said to keep it simple when you're still on the stage of getting it to work (i.e. getting a test to turn green).


Time wastage & how to avoid it

For a given requirement, a simple solution design that make us think about creating good abstractions and then Implement with testing looks very intuitive.

The intention is good but you're forgetting about the drawbacks. Taking a "design first, implement second, test third" approach is susceptible to two major issues:

  • Overengineering (designs that are more complex than they needed to be)
  • Wasted implementation effort (when the tests show you were doing something wrong)

The red/green approach inverts that order of operations, so that you can better avoid these weak spots.

  1. Write a test which compiles but fails.
  2. Implement a quick and dirty solution until the test succeeds.
  3. Improve upon that solution (refactoring) while making sure that the test doesn't start failing again.

This prevents the weaknesses mentioned before:

  • You're less likely to overengineer as you have concrete evidence on exactly how complex your implementation is (rather than guessing what it will be), so you can better tailor your design to your implementation
  • Since you can run your test every step of the way, any mistake you make is immediately noticed, and you don't find out about it much later in the process.

Direct feedback to your questions

Is TDD a brain wash?

No, but it does shake up the fundamental approach to software development. When you first encounter something that is radically different from the ground up, it's very hard to know whether this new thing is wrong or whether you just don't get it (yet).

Based on the prevalence of TDD in the field of software engineering, it's safe to say that it does work, so the logical consequence is that you're not quite understanding its principles (yet).

If you cling to the (non-TDD) fundamentals you know and love and don't want to reconsider them, then you're not in the right mindset to accept TDD as a different-but-equally-valid methodology.

Why would a good Software engineer, make the test work quickly, committing whatever sins necessary in process?

Because that software engineer minimizes time wastage while also not lowering code quality.

By keeping the implement/test feedback loop as short as possible, they maximize their efficiency at finding and dealing with any error they may have made as the error would have been made recently (as opposed to a long time ago, where your memory is fuzzier).

Why would the refactor stage be applied after completing a dumb green stage?

By implementing a first (temporary, working) solution, they more concretely define the domain for which they will then design an appropriate second (cleaner, more abstracted) solution), thus making sure they don't over-engineer things or regress constantly, both of which waste time on things that are not necessary or can be avoided.

Are those test cases(in Red stage) missing in non-TDD approach?

Red stage test cases are beneficial in two ways:

  • They force you to define an interface without using your knowledge of the implementation details (since the implementation doesn't exist yet).
  • You set up a testing suite that will then support your implementation and refactoring stages, which means you get more direct and accurate feedback about the consequences of any changes you make. Think of it like having a personal code reviewer who's constantly looking over your shoulder and immediately pointing out any mistakes you make, instead of telling you a week later. Sort of like what your IDE already does for you (the red squiggles), but using a bigger picture.

For a given requirement, a simple solution design that make us think about creating good abstractions and then Implement with testing looks very intuitive.

The design phase is comprised of two main considerations:

  • Separating the interface from the implementation, by abstracting an interface that focuses on communicating with the outside world.
  • Design patterns that streamline the internal implementation.

I want to draw your attention to the first bullet point I listed for the red stage test cases.
By forcing a developer to write tests based on an interface without an implementation, the developer is forced to design the interface in a pure manner (without knowing any implementation details, since that stage hasn't happened yet).

This already covers the first bullet point, separating the interface from the implementation. It's beneficial to do this before the implementation stage, as it means that you're not distracted by having implementation knowledge that you shouldn't be relying on when defining the interface.

The second bullet point, implementing design patterns to streamline the internal code, should only occur when there is internal code (i.e. after the implementation stage), since the choice of design pattern completely depends on the actual code being used. Here, you should rely on implementation knowledge, which is why the abstraction stage comes after the implementation stage.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.