If you ask me what is was the most unexpected thing about Test-Driven Development that I learned from the Kent Beck’s year 2003 book, I would know exactly what to answer. It will probably surprise you as well, so hop in, let’s try to take a look from a different angle.
What did you think about TDD when you heard about it for the first time?
I remember this sensation pretty well: it was a mixture of “no, you can’t be serious, how can I test something I didn’t even implement yet?”, a dull gut-located feeling of “God, writing tests is so hard” and a little bit of shame because “good programmers do automated tests” and I didn’t do much of testing at all.
It was maybe some ten years ago, I was in the third year of the Uni and I barely had any “industrial” experience. TDD and other Agile ideas were shooting towards modern problems of clogged software development processes, which were far from understanding of a Russian Computer Science student.
TDD also left this doubt about how on Earth this can be feasible in an actual project.
So why did you decide to read the book?
Some years later I landed my first “industrial” job: it was Software Engineer in Test in a big Russian Internet company. In really short time I have seen a lot of code of various quality, the code most of the time working under massive loads and bringing big buck to the company.
I was exploring the code largely from the outside, since I cared most about how can I invoke that service that returns user scores, or how can I make that data processing daemon start in one-process mode so its behavior is at least predictable during the tests.
But I also took an accidental peek into the inside. Should I say it was most of the times poorly structured, hardly readable and — tada! — without unit tests?
The question I asked to myself was, TDD seemed to make so much sense, why isn’t it applied here? Off I went to look for the answer — on the pages of the book.
What was the most unexpected thing, in the end?
Pretty early in the book Beck states that the essential process of TDD is, roughly speaking, a repetition of the following steps:
- Red tests: write tests for new features and make sure they fail
- Green tests: write enough code to make the tests pass
- Refactor: clean up the code
Red and Green steps were quite obvious, but the third one was not obvious at all. What does it mean? Why Refactoring is even there?
What did I know about refactoring prior the book
Put in simple and abstract way, refactoring is when you change the code but the external behavior of the system does not change.
But for a Junior software entwickler which I was it had a different flavour:
Rewrite this piece of legacy code that is impossible to maintain — into something more maintainable.
And this situation is pretty common between us, programmers. A CS class project that you wrote last year and want to reuse for a different one this year: Hello, me from the past, were you nuts? Why did you write that junk? Or an industrial project that has a life span of few years and whose maintainers changed few times, and nobody really knows what’s going on there.
In any case:
is the idea that usually comes to mind when the trouble is already there.
What does Refactoring mean for TDD?
TDD allows you to write silly code — remember, enough to pass tests. It unties your hands: if there is a simple way to do something, do it.
Copy-paste? Be it.
Hard-coded values? Sounds good.
We are doing this in Software Engineering all the time, one would say. But in TDD it is the code of conduct, along with the number 3: Refactor.
During this phase you remove accidental “shortcuts” from your code. Duplication? Generalise, but just enough to pass the tests. Hard-coded values repeat? Make a constant. What Refactoring does to your code is that it makes it ready for future changes.
Why is it so important to be able to test — and refactor?
One thing that “TDD by example” book should convince you about is that your code is doomed to change. In these conditions you better have tests and refactor often. So whenever the change comes, be it five minutes or five months later, be it fixing a typo or redesigning the architecture, you are prepared.
That the test which should fail will fail, and those tests that should not will not and will ensure you that your software is rock solid. That your tests are enough and passing.
That your data structures are adequate representation of your data, and you can safely rename, move, split, regroup code and be sure that the bug fixed will not introduce a hundred new ones.
So not the tests but the ability to change confidently is the key property of Test-Driven Development. With TDD you can start small and shoot the stars!
One thing: how can we start doing TDD in a legacy project?
I will first let this soak in. This question actually produces kind of a bomb explosion effect: How can one respond to THAT?
Here we come close to the understanding why TDD is not a silver bullet. It is pretty strict, so once you do not comply just a bit, you do not comply at all, and lose most of the benefits. That’s what will happen if you try to use TDD in a legacy software project. It will take enormous time to reach desired level of test coverage — and would be impossible to do safe refactoring before that.
Begin with tests. Write as many as you find fit. Try different levels of testing, anything that does the job. Write tests for new features. Praise writing tests and estimate effort required to implement the feature including the time to properly test it.
Once you are caught up it doesn't really matter if RED comes before GREEN or after.
The safe change is what matters most.