The basics of TDD

The objectives of Test Driven Development and unit testing are generally misunderstood. The problem is the word ‘test’, it is much less about testing and much more about specification of requirements, showing your working – as in maths, and the impact it has on design. TDD is much more important than only testing. Robert C Martin has a good analogy, he likens TDD to double entry bookkeeping:

Software is a remarkably sensitive discipline. If you reach into a base of code and you change one bit you can crash the software. Go into the memory and twiddle one bit at random and very likely you will elicit some form of crash. Very, very few systems are that sensitive. You could go out to one of these bridges over here, start taking bolts out and they probably wouldn’t fall. I could pull out a gun and start shooting randomly and I probably wouldn’t kill too many people. I might wound a few but — you know — you get a bullet in the leg or a lung and you’d probably survive. People are resilient — they can survive the loss of a leg and so forth. Bridges are resilient — they survive the loss of components. But software isn’t resilient at all: one bit changes and — BANG! — it crashes. Very few disciplines are that sensitive.
But there is one other [discipline] that is, and that’s Accounting. The right mistake at exactly the right time on the right spreadsheet — that one-digit error can crash the company and send the offenders off to jail. How do accountants deal with that sensitivity? Well, they have disciplines. And one of the primary disciplines is dual-entry bookkeeping. Everything is said twice. Every transaction is entered two times — once on the credit side and once on the debit side. Those two transactions follow separate mathematical pathways until they end up at this wonderful subtraction on the balance sheet that has to yield to zero.
This is what test-driven development is: dual-entry bookkeeping. Everything is said twice — once on the test side and once on the production code side and everything runs in an execution that yields either a green bar or a red bar just like the zero on the balance sheet. It seems like that’s a good practice for us: to [acknowledge and] manage these sensitivities of our discipline…
The sensitivity of software is a good point to reflect upon, there is little in human experience that is so complex and yet so fragile. Without a strong focus on showing your working, no matter how good you are as a developer, if you omit the tests, your software will be worse than it could have been.
The double-entry bookkeeping analogy only holds up though if you do test first development. If you write your test after the code it is generally not sufficiently independent to provide a valid “separate path” check.
Test first is the idea that your write the test before you write the code that is being tested. This seems like a bizarre idea to many people at first, but actually makes perfect sense.
If you write the test first and run it, you get to see it fail, so you are testing the test.
If you write the test first then you are expressing what you want of your software from the outside in. It leads you to design for behaviour and so you have less of a tendency to get lost in irrelevant technicalities.
This is a much more effective design approach than testing after you have written the code, and as a by product it leads inevitably to software that is easy to test – you have to be pretty dumb to write a test before you have written the code for an idea that can’t be tested!
Finally there is a virtuous circle here. Software is easy to test when it is modular. It is easy to test when dependencies are externalised and it is easy to test when there is a clear separation of concerns.
Now the software industry is famous for change, but if there is any idea that has remained constant for, literally, decades it is that quality software is modular, has well defined dependencies and clear separation of concerns – sound familiar? This has been how computer science has defined quality since before I started, and that was a very long time ago!
Using TDD as a practice makes you produce higher quality software, not because it is well tested (though that is a nice by-product) but because it improves the quality of your designs. Want more detail:
This entry was posted in Agile Development, TDD. Bookmark the permalink.

2 Responses to The basics of TDD

  1. Tamás Dombóvári says:

    TDD is great, but its rules and “laws” seem to be incomplete and inconsistent.
    – In the green phase we must write the simplest possible code, which is “just enough” to make the failing test pass.
    – Moreover, we are not allowed to write code without a failing test. Never. (This is one of the “laws” of Uncle Bob.)
    – In the blue phase (refactoring) we must not change the behavior.
    The result is a extremely long list of ifs. Each new failing test can be made green with a new if.
    This is, of course, nonsense, which means that something is missing. It must be possible somewhere to break the laws and write code without test. I do not think that we would be allowed to change the behavior in the blue phase. What remains is the green phase. But that means that we must break the 3rd law of Uncle Bob sometimes. But when? And why?

    • davef says:

      It’s advice for better engineering, it’s not a religion! You can, of course, do whatever you want, the “rules” are advice for how to get the best outcome. The reasons are important. but neither Uncle Bob, or I, am going to check up on what you do.

      The reason that I recommend that you write the simplest code in “green” is that you are in an unstable state, because the tests aren’t passing until “green” is complete. So if you do too much work you have a higher chance of introducing another problem that will make it harder to fix things later.

      The reason I recommend that you “don’t write a line of code until you have a failing test” is because you aren’t driving your design from a test, if you don’t have a test. TDD applies a pressure for higher-quality code – more modular, more cohesive, better separation of concerns, better abstraction and looser-coupling. So if you don’t write the test first, the probability is that you will get a worse design, and also end up with code that is not as “testable”.

      The reason that we “must not change the behaviour while refactoring” is because that’s the definintion of “refactoring” it is a “behaviour-preserving change”.
      In TDD the idea is that when we write the test, we are designing our system from the outside-in perspective of a user of our code. This makes for better abstraction, better design. in “green” we consciously try to avoid design, by implementing the simplest thing that will work, then we *design the implementation* and make it as nice as we can, all the time while we are under the protection of a passing test. If we change the behaviour, our test may not protect us.

      So, for me at least, all this seems like a coherent rationale for doing things this way. I don’t see why you think this is nonsense, but of course you can write code without writing a test first, but if you do the chances are you will do a worse job as a result, but that is still your choice!

Leave a Reply

Your email address will not be published. Required fields are marked *