Monday, March 29, 2010

The TDD checklist (Red-Green-Refactor in detail)

I have written up a checklist to use for unit-level Test-Driven Development, to make sure I do not skip steps while writing code, at a very low level of the development process. Ideally I will soon internalize this process to the point that I would recognize smells as soon as they show up the first time.
This checklist is also applicable to the outer cycle of Acceptance TDD, but the Green part becomes much longer and it comprehends writing other tests. Ignore this paragraph if this get you confused.

TDD is described by a basic red-green-refactor cycle, constantly repeatead to add new features or fix bugs. I do not want to descend too much in object-oriented design in this post as you may prefer different techniques than me, so I will insist on the best practices to apply as soon as possible in the development of tests and production code. The checklist is written in the form of questions we should ask ourselves while going through the different phases, and that are often overlooked for the perceived simplicity of this cycle.

Red
The development of every new feature should start with a failing test.
  • Have you checked in the code in your remote or local repository? In case the code breaks, a revert is faster than a rewrite.
  • Have you already written some production code? If so, comment it or (best) delete it to not be implicitly tied to an Api while writing the test.
  • Have you chosen the right unit to expand? The modified class should be the one that remains more cohesive after the change, and often in new classes should be introduced instead of accomodating functionalites in existing ones.
  • Does the test fail? If not, rewrite the test to expose the lack of functionality.
  • Does a subset of the test already fail? Is so, you can remove the surplus part of the test, avoiding verbosity; it can come back in different test methods.
  • Does the test prescribe overly specific assertions or expectations? If so, lessen the mock expectations by not checking method calls order or how many times a method is called; improve the assertions by substituting equality matches with matches over properties of the result object.
  • Does the test name describe its intent? Make sure it is not tied to implementation details and works as low-level documentation.
  • How much can you change in an hypothetical implementation without breaking the test (making it brittle)?
  • Is the failure message expressive about what is broken? Make sure it describes where the failing functionality resides, highlighting the right location if it breaks in the future.
  • Are magic numbers and strings expressed as constants? Is there repeated code? Test code refactoring is easy when done early and while a test fails, since in this paradigm it is more important to keep it failing then to keep it passing.
Green
Enough production code should be written to make the test pass.
  • Does the production code make the test pass? (Plainly obvious)
  • Does a subset of the production code make the test pass? If so, you can comment or (best) remove the unnecessary production code. Any more lines you write are untested lines you'll have to read and maintain in the future.
  • Every other specific action will be taken in the Refactor phase.
Refactor
Improve the structure of the code to ease future changes and maintenance.
  • Does repeated code exist in the current class?
  • Is the name of the class under test appropriate?
  • Do the public and protected method names describe their intent? Are they readable? Rename refactorings are between the most powerful ones.
  • Does repeated code exist in different classes? Is there a missing domain concept? You can extract abstract classes or refactor towards composition. At this high-level the refactoring should be also applied to the unit tests, and there are many orthogonal techniques you can apply so I won't describe them all here.
Feel free to add insights and items on the list in the comments. I value very much feedback from other TDDers.

7 comments:

Anonymous said...

"Ideally I will soon internalize this process to the point that I would recognize smells as soon as they show up the first time."

There's something I've never understood about TDD.

TDD has, I suppose, two main goals: improve design, and produce a set of unit tests.

Let's ignore the, "Produce a set of unit tests," as that's achievable with test-after-design.

When you do TDD often enough, you realise that the methods you write tend towards a specific property, which goes by many names, but we'll call it here: isolation. If methods are isolated, they're easier to test.

So why is it that no amount of TDDing allows TDDers to internalize this, and simply write isolated methods in the first place, without having to write the tests first?

What does TDD offer beyond this method isolation?

I think it's a shame that TDD is so psychological; software development will continue to flounder so long as subjective methods hold sway.

The scientific approach to software development would be to identify the objective benefits (i.e., method isolation) brought by any design technique and standarize that benefit, rather than standardize a psychological preference (TDD over test-after design) which only brings about the benefit as a side-effect.

TDD is a pair bicycle training wheels. Once you learn how to write methods properly, the training wheels are no longer necessary. All good programmers either stop practicing TDD or never learn the benefit that it brings to method writing. Thus, TDD is the sign either of a novice or someone with a learning disability.

Giorgio said...

...while others think that using TDD is just like a surgeon washing his hands, or an accountant using double-entry bookkeeping:
http://blog.objectmentor.com/articles/2009/10/06/echoes-from-the-stone-age
"All good programmers either stop practicing TDD or never learn the benefit that it brings to method writing. Thus, TDD is the sign either of a novice or someone with a learning disability."
Apart from the fact that a new word (test-infected) has been created to address the developers who enjoy TDD and never stop doing it, this comment seems like trolling to me. Really no serious person would take neurological disorders (which are serious issues) into a discussion about software development. :)

ferrisoxide said...

Re Anonymous's comments, isolation isn't the only benefit TDD offers design - nor would I say it's the most important. First and foremost is the simplification of your design. Driving your design via unit test will generally produce a simpler and more coherent design than "test after" because you've constrained yourself to just satisfying what the tests demand.

TDD is also about discipline - of having the will to go back and improve existing code through refactoring. Refactoring without tests is risky - you've no idea what you have broken. And keeping to the TDD cycle of red-green-refactor gives your coding a rhythm to continually move forward with, improving as you go.

This is not rocket science - it parallels Plan-Do-Check-Act cycle commonly used in business process improvement. TDD creates an important feedback loop that aids improving software development - without feedback it's difficult for any system to improve. I'd suggest this is more scientific in spirit than the hack-and-move-on style of many programmers. And not everything has to be "science" anyway - the benefits of writing cleaner, proven code may intangible but if I sleep better at night why do I care?

Having recently come back to a company that dropped TDD while I was away, I'm now dealing first-hand with the consequences of leaving testing to the end of the process - or not at all in some cases. The code is in part bloated, buggy and in general very clever.. but I have no confidence I can improve the bad code without breaking the good parts of it.

TDD is a tool for a specific set of purposes - and like anything faddish has the potential to be overused (or worse, misunderstood AND overused). It is less a pair of bicycle training wheels and more a space to discover the better ways to code - method isolation being one. But it has a great many benefits above and beyond just method isolation. Some of the things I've found TDD useful for:

- making your assumptions explicit
- reducing the amount of code
- building quality into the development process
- proving a bug exists before you go fixing it (and subsequently proving that you have fixed it)
- communicating with peers and management
- measuring success

Now none of this is scientific, but I can say from an empirical point-of-view, having worked at shops where TDD is part of the culture and where it's not, TDD at least promotes behaviour that helps teams work together. TDD may be "psychological" - but it at least takes into account that software development is a human activity, not an academic or abstract one.

Anonymous said...

Hi, Giorgio,

Thank you for your feedback comment.

I didn't understand, however, the reference the, "Test-infected," as I
didn't use that phrase in my comment.

I remember reading the UncleBob post you reference and was
saddened, for the reasons I mention my post.

And I apologise if you thought my post was trolling; I merely meant to
state why I find the path away from objectivity so unfortunate.

Yours, AnonymousAgain.

Anonymous said...

Hi, ferrisoxide,

Thank you for that excellent comment. I hope you will bare with me
while I take some of your points in turn.

I'm afraid I have to separate my comments into multiple parts to fit the 4096 character limit: apologies.

FerrisResponse Part 1:

You mention that the benefit of TDD, "... First and foremost is the simplification of your design." That is one of the other terms instead of, "Isolation," that I was loathe to mention above, because, "Simplicity," is not well-defined and subjective.

It would be great, however, if you could offer an objective measure of simplicity (even a local, non-generalised definition would be fine). Then we can actually test your thesis and have two people write code - one TDD, one test-after design - and make an objective measurement to see who wins.

The only problem would be that once we identify your objective measurement of simplicity, and once we both agree that the TDD-generated code is superior to that produced by the test-after approach, then we could run another experiment betweeen TDD and test-after, but this time the test-after chap would know the objective simplicity that your TDD highlighted and he could code that simplicity
into his design.

An objective measure of the output of this second experiment would then reveal one of two things, either:

A) The TDD code and test-after code are now equally simple, but the TDD code is better because of a different, separate objective quallity - let's call it quality Q2 - which it displays but the test-after code does not; or

B) The TDD code and the test-after code are now equal in simplicity, thus obviating the need for TDD.

And if (A) is the case, and a new Q2 quality has been unearthed, then we can run a third test, with the test-after designer writing but simplicity and Q2 into his code, and again objectively measure the differences.

And we can repeat until all qualities are covered; at the point when all qualitities are covered, then if the TDD code is still superior, then the test-after coder has failed to learn how to implement one of the qualities (hence the learning-disability) or else there are an infinite number of qualities to learn about computer programs and TDD covers them all but test-after coding does not. I doubt that the latter is valid, nor do I know of any prominent TDDer who claims such a thing, hence TDD's being useful only while coders learn the
qualitites it bequeaths, thereafter its usefulness declines to subjectivity.

Admittedly a rather silly thought-experiment, this just tries to show why I think TDD's usefulness is limited and may even be counterproductive after a time.

"TDD is also about discipline - of having the will to go back and improve existing code through refactoring. Refactoring without tests is risky - you've no idea what you have broken." True, but so is test-after design, which no less employs refactoring, but does so, in some case, without the need to refactor the tests that were written before the design, hence reducing the time needed to produce the software.

"And keeping to the TDD cycle of red-green-refactor gives your coding a rhythm to continually move forward with, improving as you go." This is subjective; some people feel the red-green-refactor cycle a great frustration and an waste of time.

You mention that TDD, " ... parallels Plan-Do-Check-Act cycle commonly used in business process improvement." So does test-after design.

"I'd suggest this is more scientific in spirit than the hack-and-move-on style of many programmers." True, if slightly staw-man-ish, but so is test-after design.

"And not everything has to be "science" anyway." True, though objectivity has some benefits over subjectivity.

Anonymous said...

FerrisResponse Part 2:

" - the benefits of writing cleaner, proven code may intangible but if I sleep better at night why do I care?" This is a curious statement. Firstly, I agree, there is the very real subjective benefit of TDD: some people feel better doing it. That's a perfectly valid reason for continuing to practice TDD; but it's also a perfectly valid reason for the practicing the hack-and-move-on style of many programmers (some people feel better doing that, too), which you decry. Secondly, code is seldom, "Proven," by running tests, unless all possible inputs (to the machine's limit) are individually tested: seldom a cost-effective approach. Thirdly, if you'd permit me to use, "Better," instead of, "Cleaner," ("Better," being slightly more objective, though not much) then the benefits of writing better code most certainly tangible, in hour/dollar savings.

"TDD creates an important feedback loop that aids improving software development - without feedback it's difficult for any system to improve." True, but test-after design also creates that feedback loop.

"Having recently come back to a company that dropped TDD while I was away ..." A perfectly valid anecdote, but an anecdote nonetheless; I'm sure we could swap anecdotes about TDD-gone-good versus TDD-kicked-out-the-window all day; we would not resolve the issue. But nice anecdote, anyway.

"TDD is a tool for a specific set of purposes - and like anything faddish has the potential to be overused (or worse, misunderstood AND overused)." So does test-after design.

"It is less a pair of bicycle training wheels and more a space to discover the better ways to code ..." I fully agree. TDD is a great space to discover better ways to code; but once you've learned those better ways to code, there's no compelling reason to continue TDD, and there's compelling reason to concentrate on the better ways of code that you've learned rather than the mechanism through which you've learned them. This is simply more efficicent.

Some of the things you've found TDD useful for:

- making your assumptions explicit; great, but once you've learned this, why not concentrate your efforts on making your assumptions more explicit, rather than using the mechanism that showed that this was a benefit. Perhaps by concentrating on making your assumptions more explicit, you'll learn how to more efficiently make your assumptions more explicit, which may not be possible by the continued practice of TDD?

- reducing the amount of code; great, but once you've learned this, why not concentrate your efforts on reducing the amount of code, rather than using the mechanism that showed that this was a benefit. Perhaps by concentrating on reducing the amount of code, you'll learn how to do so more efficiently, which may not be possible by the continued practice of TDD?


- building quality into the development process; great, but once you've learned this, why not concentrate your efforts on building quality into the development process, rather than using the mechanism that showed that this was a benefit. Perhaps by concentrating on building quality into the development process, you'll learn how to do so more efficiently, which may not be possible by the continued practice of TDD?

Anonymous said...

FerrisResponse Part 3:

- communicating with peers and management; great, but once you've learned this, why not concentrate your efforts on communicating with peers and management, rather than using the mechanism that showed that this was a benefit. Perhaps by concentrating on communicating with peers and management, you'll learn how to do so more efficiently, which may not be possible by the continued practice of TDD?


- measuring success; great, but once you've learned this, why not concentrate your efforts on measuring success, rather than using the mechanism that showed that this was a benefit. Perhaps by concentrating on measuring success, you'll learn how to do so more efficiently, which may not be possible by the continued practice of TDD?

- proving a bug exists before you go fixing it (and subsequently proving that you have fixed it); this is a dubious benefit, the extent to which it can be employed (if at all) is subjective. Let's say I write a for-loop from 0 to 99, and afterwards test that it works from 0 to 99. There is clearly little benefit in first writing a test to make sure that a for-loop works from 0 to 99, then writing a for-loop from 0 to 99 that exits at iteration 57, then confirming that the test fails, then removing the exit at 57, then confirming that the for-loop works. If this approach is to differentiate TDD from test-after then you must demonstrate how TDD catches bugs that test-after does not, otherwise this TDD approach can be made to look like a laughable and significant disadvantage of TDD. And in any case, this definition of, "Bug," is extremely broad; you're classifying the for-loop that you've not written yet a bug which devalues the term almost to insignificance. I would certainly agree, however, that TDD proves that the code you haven't written yet doesn't exist, I just question the benefit of, need for and investment in such proofs.


"Now none of this is scientific, but I can say from an empirical point-of-view, having worked at shops where TDD is part of the culture and where it's not, TDD at least promotes behaviour that helps teams work together." Admittedly anecdotal, but great, then why not concentrate your efforts on helping teams work together?

"TDD may be psychological - but it at least takes into account that software development is a human activity, not an academic or abstract one." Apart from academic activities also being human activities, I would not have thought that roaring, "TDD takes into account that software development is a human activity rather than an abstract one!" from the roof-tops would have programmers rushing towards you in their droves, but I may be wrong.

ferrisoxide, some critical thinking reveals no significant, valid argument in your comment, however excellently prepared.

If those who do not practice TDD are, "Living in the stone-age," (to use UncleBob's expression) then surely the arguments for TDD would be as unassailable as those for using a chain saw to cut down a tree as opposed to a stone axe.

Yet they are apparently not.

Why do you think this is so?

Yours, AnonymousAgain.

ShareThis