My personal experiences with TDD mostly match with the others on the internet, in short, TDD is good. It helps you to write better code, create a clean and nicely tested architecture, make refactoring and design changes easier. It leads your design decisions, helps to think through every possible cases which you need to handle and many more. I don’t want to repeat the same things which already have been written a hundred times, instead of I try to share some personal thoughts.
Disadvantages of TDD?
Not everybody agree with my first statement, for example: http://programmers.stackexchange.com/a/98566
First it’s hard, make things slower, need to practice, yes, that’s true, but most of the complaints not related to TDD.
For TDD, need to learn just a few important things, how to work in little steps and how to think upfront, latter is necessary anyway for a good design.
The problem is, that people thinks TDD is an independent skill, it does not require any background, but that’s not true.
Maybe you already heard this: “We must learn to walk before we can run.”
Same with TDD, first need to learn how to write unit test at all, then write them in a TDD way. Most complaints are based on poorly written unit test, not on the TDD process itself. Like high coupling, testing implementation details, overmocking things etc. Those are the shortcomings of the developers, not TDD faults, but for most people, it’s hard to face it, so it’s easier to blame something/somebody else.
When we know how to write good unit test, just need some practice for the next step, therefore I highly suggest to do some TDD kata, e.g.: http://osherove.com/tdd-kata-1/
Only for new code?
After all of this, doing TDD on a new code is easy, so I want to say some word about the worst case, when need to make changes in legacy code, which doesn’t have any unit test.
The first question, is it really related to TDD? We need to cover the old behavior, before writing the new one, so this won’t be driven by test, because the code is already written. Yes, that’s true. We are doing that to protect ourselves when we will implement the new functionality, but most importantly, it helps us to practice the same thought process which is necessary for doing TDD right.
Refactoring, the TDD way
The following technique is useful when the code is not too bad, e.g.: it’s not a god class, with 3000 line and 1 public method and everything created with ‘new’, inside the method bodies etc. The essence of the technique: always try to find the shortest branch and write a test for that. If the class has multiple public method, then find the shortest/simplest one. Then look for the shortest branch which has some behavior/decision point. The simplest form obviously is an “if-else”, e.g. if the “else” branch just set a variable or throw an exception, test that first, then go to the “if” part and find the shortest there.
Ok, but what to do when there is no “if”?
Depends on the code, but some suggestion. Does it have an early return point? Then force to use that branch. Does it have some loop? Can you force it to skip the loop(e.g. with an empty collection)? Then do that. Can you force it to throw an exception? Then do that. Use the same thinking when it call a method, try to find the shortest branch inside that method too and so on.
How is this related to TDD?
Doesn’t sound familiar?
- We write a test for an assumption and may or may not watch it fail, depends on missed some expectation or not
- We go forward in baby steps, always write test for the smallest possible piece of code. Of course this mean, we don’t build up the whole test environment in the beginning. I mean, if the class has 10 dependencies, then we don’t create mocks or build those classes in the beginning, just the first time when want to use them. We must be sure, that we don’t need to change the already written tests to introduce a new dependency, because until that point we didn’t have to use it.
- When we first use a new dependency, set some expectation, then write it in the current test. Next time when we write the same, then we should start to think to put it in the ‘setUp’ method, which mean, we refactor the test code in every iteration. And the best part of this, that we can estimate how to handle that particular expectation, because we see how many times it will be used in the code. If we see, that it’s a really big branch and need to set the same things in 80% of the tests, then we can put it in some setup method, but if that is only a short branch and need to use it only in 2-3 tests, then repeat it instead in every scenario.
- We build up the knowledge for the old code behavior through these safe, little steps, like when we build up the code, through TDD.
So I think it’s some kind of reverse TDD technique :) It does the job quite well, so I highly recommend, when next time you facing with a similar code base, give it a try.