A walkthrough of how TDD can be badly applied to promote bugs, and an alternative. I'm imagining that my project is in Lisp, for the code samples, and so that I have a decent REPL (read-eval-print-loop, a place where you type code and see what it evaluates to). The situation described here is only partially based on reality, but you can judge how likely it is for yourself. 4:40pm Let's consider some code that we completely understand - adding two 2D points together. TDD says that I should write a test.. ok: (assert (equal (point+ (point 3 2) (point 4 6)) (point 7 8))) Then it says that I should make the test fail:
(defun point+ (first second) (point 1 2))Ok, that fails. Now I should make it pass, and I should do the minimum required for that.
(defun point+ (first second) (point 7 8))4:45 It passes. I get distracted by the telephone. 4:55 I look back, and I see that my test passes. Ok, I'm done, let's move on. None of my other tests use point+, because I try not to write dependent tests. So in this case, I will find the above bug in integration tests. But those involve files, and are a bit slower, so I don't bother running those quite so often. 5:00 My boss comes in and asks for a build for today's work, so that he can have a play around with it on his laptop on the train, which sets off in 10 minutes. I disable the integration tests temporarily so that I can just build it and give it to him, then dump a jar file to his USB pen drive. 5:10 He's on the train. 5:20 I'm distracted by the phone again. "All the drag and drop seems to have suddenly started going to the same place". Ok, I say, I'll fix it right now. So I re-enable all the integration tests, and yes, there's a failure. I fix it. 5:30 I email another build to my boss, but he's not able to get it until he's at home. While he was on the train, he gave a broken demo to a potential customer. You can find a few things wrong with my environment, such as that I don't use continuous integration, I actually answer phone calls while coding, and that I don't keep daily builds around for those times that my boss asks for a build. However, those are fairly superficial. The big problem that following TDD caused me was that I felt good about some really bad code. Without a test, I would have had suspicion for the code, but the test gave me confidence. That's not to say that testing is a bad thing, but that TDD gave me confidence in broken code. So let's implement the same thing again, but with a more 'distraction-proof' testing technique: 4:40 Write the function:
(defun point+ (first second) (point (+ (x first) (x second)) (+ (y first) (y second))))4:45 Check that it works for a few values in the REPL: (point+ (point 1 2) (point 3 4)) > 4 6 (point+ (point 1 4) (point 5 10)) > 6 14 ok, seems fine. 4:50 Get distracted. 4:55 Return to the code, there's no test yet, so I don't trust point+. Oh, but wait, look at the REPL, it looks like it works. I run the tests in the REPL again, then write a test case. (assert (equal (point+ (point 3 4) (point 5 6)) (point 8 10))) The test case passes, and this time I haven't made incomplete code pass tests. My boss, on the train, is happy, and so is the potential customer he demoed the software to. A REPL helps me to separate code that's part of my project from code that is just being thought about. It encourages experimentation, and hence thought, but doesn't get in the way should you get distracted and come back. It also makes you less likely to put broken code into the codebase, because it doesn't make you write experimental code in the production code.