Direct and Indirect Effects of TDD

Some time ago @unclebobmartin tweeted about the direct and indirect effects of TDD:

TDD guarantees test coverage.
3:17 PM Jan 31st
TDD helps with, but does not guarantee, good design & good code. Skill, talent, and expertise remain necessary.
3:25 PM Jan 31st

I agree with the above, but I also felt that there was still something missing, because I did not see a direct relation from good test coverage to good design - it's possible to get high test coverage even with test-last approaches, but that does not help with the design similarly to TDD. So that made me think about what are the direct effects of TDD, and how do they indirectly help with the design.

Here are some effects that I've noticed TDD to have, divided into direct and indirect effects. If you have noticed some more direct or indirect effects, please leave a comment.

Direct effects, given just following the three rules of TDD:

  • Guarantees code coverage
  • Amplifies the pain caused by bad code

Indirect effects, given skilled enough programmers using TDD:

  • Enables changing the code without breaking it
  • Improves the quality of the code

Direct: Guarantees code coverage

From the three rules of TDD it's easy to see that no application logic will come to existence, unless some test first covers it. So if somebody just follows these rules with discipline, code coverage is guaranteed. That result is irrelevant of the skills of the developer*.

* Although it will be hard for a very unskilled and undisciplined developer to follow the rules, but that's beside the point. ;)

Direct: Amplifies the pain caused by bad code

In TDD, after writing a failing test, the next step is to make it pass with the simplest possible change - the code does not need to be elegant, because it's meant to be refactored later. Generally there is very little thinking before writing some code (just a rough idea where the project is heading), but instead most of code design is meant to be done after writing the code. Also, to be able to test something, the code needs to be testable - in other words low coupling and high cohesion.

The above leads to TDD requiring existing code to be changed continuously - it's like being in maintenance mode all the time. So if the code is not maintainable, it will be hard to change. Also if the code is not testable, it will be hard to write tests for it. If the code would not be written iteratively and no tests would be written for it, life would be much easier to the developer. In other words, TDD increases the pain caused by bad code, because it's not possible to avoid changing the code, nor avoid writing tests for it.

Amplifying pain might seem like a bad idea, but actually that's one of TDD's best assets. :) Keep on reading...

Indirect: Enables changing the code without breaking it

The direct effect of code coverage makes it possible to notice when something breaks. But that alone is not enough for changing the system safely. It requires skill to be able to modify the code in small, safe steps. The developer needs the ability to do even big design changes by combining multiple small refactorings*, so that the tests pass after every refactoring. This requires skill and discipline. An unskilled or undisciplined developer would get stuck in refactoring hell, or would give up and abandon the test suite.

* Programming can be thought of as the process of solving a big problem by combining multiple small elementary pieces (such as conditionals, statements and libraries). In refactoring the elementary pieces are small transformations of the source code's structure which preserve its observed behaviour (rename, extract method, move field etc.). In this sense also mathematics requires similar thinking (for example prove a conjecture by combining theorems and axioms). This kind of problem solving requires first creativity and intuition to get an idea of the solution, and then discipline and attention to detail to implement the solution; two opposite personality traits.

Indirect: Improves the quality of the code

The direct effect of amplified pain and the indirect effect of making safe changes enable the improving of the code quality. The important point is "listening to the tests". When something is painful while doing TDD, you should be sensitive to notice the pain and then react to it by fixing whatever was causing that pain.

Growing Object-Oriented Software, Guided by Tests says on page 245 under the subheading "What the Tests Will Tell Us (If We're Listening)", commenting on somebody who was suffering from unreadable tests, up to 1000 lines long test classes, and refactoring leading to massive changes in test code:

Test-driven development can be unforgiving. Poor quality tests can slow development to a crawl, and poor internal quality of the system being tested will result in poor quality tests. By being alert to the internal quality feedback we get from writing tests, we can nip this problem in the bud, long before our unit tests approach 1000 lines of code, and end up with tests we can live with. Conversely, making an effort to write tests that are readable and flexible gives us more feedback about the internal quality of the code we are testing. We end up with tests that help, rather than hinder, continued development.

Also Michael Feathers says in an interview:

It's something that people don't talk about enough and it seems like particularly in TDD, there is a really great thing that you notice that if something hurts when you are doing TDD, it often means that it's an indication of something wrong with the design. Since people are so drawn and they say "OK, this is kind of painful. It must mean that the TDD sucks." In fact, there is a way of going and getting feedback about a thing you are really working on, if you pay attention to the pain.

Noticing the pain as soon as possible and then fixing the problem - whether it is a rigid design, fragile tests or something else - requires skill. Not everybody is alert to the pain, but instead they keep on writing bad code until making changes becomes too expensive and a rewrite is needed. Not everybody fixes the problem when they feel the pain, but instead they implement a quick hack and leave an even bigger mess for the next developer. But for those who have the necessary skills and discipline, TDD can be a powerful tool and they can use it to write better code.