How test first development changed my life
Published 4/22/2010 8:00 PM by Toran Billups 19 Comments
Since 2007 I’ve spent a large part of each day trying to understand test driven development. Looking back now, I think the reason I started down this journey had something to do with my experience trying to maintain a large application written by another developer. I still remember how hard it was to understand what each method/class was doing. And when I would make a change to existing code, I wouldn’t know about a bug until the end users found it. In short, it was this experience that got me interested in writing more maintainable software.
Initially, I thought the value of TDD would be found in a suite of regression tests to help the next developer make changes. And this is helpful no doubt, but instead what I’ve found over the last few years is that software built test-first is more maintainable than those built without testability in mind. And this is important because the maintenance of software is always more expensive than the initial development.
The first thing I learned about TDD is that it’s not about testing. It turns out that having a word like "test" in the name implies you only need to write tests to be successful. But the real goal of TDD is to accomplish higher code quality by forcing you to write a test for something before the implementation. This way you are cognizant of the complexity you are building into each class/method, well before you move on and call it "done."
When you write software test-first, you are using the tests as a design tool to help guide you along the way. And if you ask someone who has been doing test driven development for any length of time, they will tell you that this is why they follow the discipline. My own experience has shown me that this idea works in practice because the software you end up with is much easier to read, understand and maintain.
Because I’ve spent so much time learning how to write software test-first, I wanted to share how this shift in thinking has changed my life.
If you can’t write a test for something you don’t understand it
When I started writing software for a living, I got into the habit of writing code before I fully understood what the customer wanted. In practice, this meant I was writing software without thinking about the responsibility of each class/method.
This came to my attention when I tried to write a test for the first time and couldn’t give the test a name that explained the intent. It was at this moment that I fully understood why so many people feel that the hardest part about writing software is providing a good name for each variable/method/class/test.
Writing a test first required me to slow down and think more about each problem I was trying to solve. When doing this, I found the software I was writing to be of higher quality. This enabled the developers who followed to not only read it, but also understand what I was doing and make changes to it.
Another thing happened when I had to think more about what I was doing. I suddenly had a lot more questions for the business about what they really wanted. This allowed me to provide more input and, in turn, a better product for the customer.
Without a regression test you can’t clean the code
In the past, I feared having another developer read my code because it was such a mess. And if I tried to refactor it, I would have to manually test large parts of the application to be sure I didn’t break anything. But after I started writing tests first, I could refactor the production code and know if I had altered the behavior in any way. Now initially you might not think this is a problem because you only write new features, but what I found in the real world is that this can start to harm a codebase, regardless.
For example, when I was adding a new feature a few years back, I needed about 60% of the functionality in an existing method. But the method itself was over 2000 lines long, and I was afraid to refactor this to reduce duplication, so I copied and pasted it into another class. Then I tweaked it to do the "new" stuff that was required of the feature. So because I had no way to verify the expected behavior, I made a poor design decision out of fear.
Fast feedback cycles save time and money
Until late last year, most of my work experience could be categorized as waterfall. This being the idea that we do a great deal of design and analysis up front, then write the software for a year without talking to the business. I would describe this as a slow feedback cycle because the business doesn’t see anything until it’s completed. And often times, the business doesn’t fully understand what they want until they see something they don’t. And by that point it’s often too late.
The same idea applies to testing. If you are writing a test first, and you keep your tests running quickly, you will know when you are done with a feature/story/bug/etc. Without this kind of rapid feedback, you need to open the application, click around until you get to the part you modified, and see what happens. Doing this over and over again really slows down a development team, and it feels like waste.
Another great thing about this feedback is that you truly feel like you’re getting work done each hour. If you have ever worked on a long project, you understand that it can take days or even weeks to know when you’re "done" with a task. Breaking down a large problem into several smaller ones is always a good thing in my book.
You are going to test it anyway, spend the time to do it right
After a new feature was added, I would open the browser and fire up the application to make sure it worked. What I found with test-first development, was that I was already doing the same thing with the added benefit of a regression test for the next time around. Without a suite of these regression tests the application will take longer to test as it grows. At some point this will become unmanageable because you will be spending more time doing manual regression testing than you do writing new features.
You might be thinking, sure I can build a regression test suite using test-after development. And to some extent, this is true. But if you choose to spend your time writing tests after development, you will be giving up the largest benefit of all: design. Remember we are using test driven development as a design tool more than a testing tool.
Another good reason not to write tests after development is that you will never get to it. Like anything in software development, the closer you get to the end of a project, the more things get cut. This would include ALL tests if you plan to test after development. Plan ahead and test first, to avoid an empty suite of regression tests.
In addition, most of my test-after experience has resulted in much larger integration-like tests. These can be harder to understand and maintain, and often run very slow. I’ve also seen the implementation become so complex to test that I simply gave up and pushed it to production without any coverage for the next developer.
Now if you are new to unit testing, you will no doubt start with test-after simply because you have so much to learn. I started this way, as I’m sure others have. It’s just a stepping stone on your way to test-first development. Keep in mind that you want to move out of this stage quickly because bad habits are hard to break.
Without tests the next developer will need a word doc and a lot of luck
I was looking at some source code for a function I wrote myself just a year ago and couldn’t understand what it was doing. I was trying to explain this to another developer at the time and found myself going to the unit test for clarification. A unit test can be a great form of developer documentation because it shows the inputs and expected outputs.
Often times when I’m reading code, I just want to know what something does, not how it does it. To this end, unit tests provide a great value. And if you are working test-first, it helps you think about the problem from a unique perspective.
Evolutionary design is possible without fear
I took a new job last year that has me shipping software each month. Initially, I was afraid to make changes in the codebase because if I broke something, it would hang around for a month. And if this was something that would cost the company money, it might be a sign that I’m not providing the value they hired me for.
What I found instead was that we have a pile of regression tests, and everyone is working to improve the code through evolutionary design. This gave me a great deal of confidence to refactor existing classes and methods throughout my day. And if a codebase doesn’t have the ability to improve, it’s only a matter of time before the big "re-write". And often the re-write is a failure, so I plan to avoid this at all costs.
You will actually write less code
When you first talk with a co-worker about writing tests, the knee-jerk reaction is always "I don’t have time to write more code". But with modern IDE’s and refactoring tools like ReSharper, I have found that you actually write less code when you start with a unit test.
For instance, when you write a test for a class that doesn’t yet exist, ReSharper will stub it out for you with a simple keystroke. The same goes for methods and properties. So instead of writing a ton of boiler plate method and class signatures, I only write tests and the simplest implementation to get them passing.
Job security
If you approached your boss today and asked if he wanted software that was bug-free or software that was riddled with imperfections, he would hopefully say "bug-free". This is because with each bug the development team is slowed down and has less velocity to work on things that actually provide business value.
But to be clear, the tests themselves don’t actually prevent bugs from happening. Instead I have found that when you slow down and write a test, it starts to act like a specification of sorts. And when you start working to a spec with quick feedback, you examine the edge cases with a little more care than usual. And this attention to detail is what prevents bugs.
And with automated tests becoming the norm, it wouldn’t hurt to learn something new this year. If nothing else, it will help you standout at the next job interview. Believe me, this is how I landed my dream job.
It actually makes my work more enjoyable
Personally I have found "red, green, refactor" to be a breath of fresh air in our industry. I enjoy the rhythm of writing a test, watching it fail, writing enough code to make it pass, and finally cleaning it up. It makes me feel like I’m turning around something of value with each passing test.
In addition, I’ve started to understand that passing software to another developer without tests is like saying "good luck dude". I encourage everyone in the software industry to work as a professional and look out for the next developer. So instead of staying "good luck", say "hey I’ve got you covered".
Wrapping it up
TDD is a great way to build maintainable software, but it’s still not a silver bullet. What test-first development can’t verify is the end-to-end behavior of the software. I’ve learned that some form of acceptance-testing is required to fully cover integration areas and prove that the system works as the business expects it should.
That being said, I’ve found that a combination of TDD and acceptance-testing can provide great value to any development team. The business begins to trust again, and this provides a better work environment for everyone.
Lame
Test Driven Development is a pseudoscience that does a horrible disservice to correction verification of software. It is no coincidence that advocates of TDD are always projecting the discussion in the context of impractical type systems such as Java, C#, Python or Ruby. I strongly advise you to learn a practical static type system, such as that purported by Haskell or ML language family. After this, learn a theorem prover such as Coq or Agda. Did you know that for some functions, it is *impossible* to write tests *because the types prove the program*? I'm happy to help out if you choose to truly endeavour to "change your life." That is, to put aside the TDD nonsense and come to an understanding of the essence of proving program properties correct.
TDD only works if everyone is on board. If you have 3 developers and 2 are doing TDD the 3rd will make your code not testable. TDD is by far the best development tool I have used and I am 100% for it. However, teaching old dogs new tricks is an up hill battle. To truly piss people off try paired development with TDD ( within reason ) and you will find that is even better as it keeps people focused and writing even more quality code. Quality Code = Testable code code covered in unit tests.
The problem with this post is the same problem as with 99% of programming blogs: an author assumes that the experiences in his given field of programmign apply to all kinds of programing. F.e. not everybody is using an OO programming language. Some of us are stuck with a procedural system. F.e. in many cases it is more important to write a feature as fast as possible than worrying about its later maintainability, for example when you are billing by the hour and the project is already over budget and you need to avoid enraging the customer. F.e. TDD seems to have strongly algorythmical programming in mind: when you are using the computer to actually compute stuff. In cases where almost every second line is an I/O or a library call to some library that sends an e-mail or something such things would be very hard to test. And so on. There are really many different cases, scenarios.
Nice lucid article on TDD Toran. My concern is what happens before TDD? Meaning, where is the design? I contend that if the design is blown from the beginning, then TDD only verifies that the crappy design works as coded :-) The fact that one of the methods you mention is 2000 LOC is a dead giveaway of a poor deign, with or without TDD. So good article on TDD man! I would be interested on your views of software design before (TD) development occurs… How do you verify that your deign is good?
Everything can be tested. Also, everything can be tested-first. But I urge you to take note of your current programming environment, the task at thand, the languages you use, and then decide on a cost/benefit analysis. The danger of TDD is that you end up spending a lot of time with tests; complicated tests. If you could have gained 90% of the test benefit from some other tool in 10% time, it would definitely be worth it, right?
The problem with this post is that your initial focus is on just plain bad development practices, and use that as evidence for TDD: Just plain bad things: "This being the idea that we do a great deal of design and analysis up front, then write the software for a year without talking to the business." "I got into the habit of writing code before I fully understood what the customer wanted." "I was writing software without thinking about the responsibility of each class/method." "... having another developer read my code because it was such a mess" TDD may have forced you to write better code, but it doesn't mean you can't write good code without TDD. It'd be helpful to start off with the universal benefits such as if you don't write tests up front, you never will, that they can aid in re-factoring (especially if you have awesome tools), etc. It'd also be nice to talk about the negatives: e.g. you're writing 100% tested software you might throw away an hour later as your design evolves. Thanks.
Great post.
Ive just started reading about TDD and to be honest i agree with the author. It does help you better understabd what each method/problem you are trying to solve does and it does make you write better code in the end. TDD should just mean tests in the sense of unit tests, but tests in any way. Using theorem provers or anything else is also some form of testing. TDD should just be consider as an any test first development thing/principle. For instance coding web stuff i find behaviour drive development with a combination pf tests, refactoring to be great. But behaviour driven development in designing software for financial analysis etc is a no go and even TDD isnt enough. For concurrrent systems, theorem provers is where its at but that doesnt mean tests are a bad idea either. Not as easy to explain or teach someone theorem provers but its easy to explain tests units and meet half way. Some are programmers and others take into proving things work or should work. Anyway i hope i make sense. To the author i liked your post and agree. :)
Sorry about the grammar typing this on my phone isnt easy:). Thanks for the article
I just want to see all you flamerz cage fight Toran. P.S. Great Article.
@Tony, interesting use of 'impractical' there. I work on a product containing over 2M lines of C++ and C# code, which generates several $100M dollars worth of sales each year. I consider this evidence that these languages are sufficiently 'practical' to actually Get Stuff Done. What would be impractical would be to attempt to rewrite the whole thing in Haskell and prove it correct. That's not to say that either Haskell or proving programs correct are bad - I enjoy tinkering with the former, at least; nor that C# is the be-all and end-all of software development (I would really like some equivalent to Maybe instead of possibly-null references) but sometimes you need to work with what you have.
grover, I didn't say impractical language; I said impractical type system. However, they are impractical languages are your attempt at providing evidence to the contrary is a failure. Not that I wish to pursue this course of discussion. If we are to start talking about big numbers, then I have even bigger ones, but again, this is irrelevant. Instead, I recommend you learn a practical type system. Please, try it. Then see if TDD continues to "change your life." It's a pseudoscience and it cause huge detriment to the practice of software development. Like I said, I'm happy to help out where I can.
Ah, an article from a practitioner that says that TDD is not sufficient to assure quality. This needs to be stated more often. TDD is just one of many possible quality-centered approaches to design and implementation.
The problem with people like Ben is that they assume that someone else's positive experiences have to also apply to the scenario they're in. If you're not smart enough to figure out that Toran was talking about OO (there's a strong hint in the first paragraph), you should just move onto a place where you can rain on a slightly less bright parade.
The problem with people like EJ is that they assume there is consensus on good and bad practice. Attention pious people shitting on this blog post, get a clue: It's something the author has found to be a boon to his learning more about development, who are you to judge that it has not been? I've seen how people like EJ code, and I'd put Toran's code up against his lame coding skills any day. I've seen many people champion the "bad practices" that EJ rails against as good practice. Oh, and for those who want to shit on TDD because it's only useful in languages that the vast majority of development shops code in, get an anti-pomposity clue.
I see Ben duplicates his "TDD doesn't solve everything, so I won't do it" rant from reddit. Basically it's just a long list of excuses. If it doesn't apply to you and your shop, don't use it, but reality is that it works just fine for a large number of applications, even those where some fearful old dog wanted to stick in their comfort zone and not even try. ... BTW, as soon as you hear something like "TDD seems to have," you know you are listening to someone posturing without a leg to stand on (i.e. someone who hasn't even done what they are railing against). ... Ben says, "in many cases it is more important to write a feature as fast as possible ... for example when you are billing by the hour and the project is already over budget and you need to avoid enraging the customer." Yeah, sure, Ben. How many times have you personally coded a quick fix that created more problems that it solved, further enraging the customer?
@Tony: formal verification, proving program behavior, theorem provers, not using Java or C# etc: 99.9999% of developers will agree that these are totally impractical. They are also of no use from a theoretical point of view: formal verification and proving program behavior indeed are undecidable problems---in computability theory, Rice's theorem proves that, for any non-trivial property of partial functions, there is no general and effective method to decide whether an algorithm computes a partial function with that property; Rice, H. G. "Classes of Recursively Enumerable Sets and Their Decision Problems." Trans. Amer. Math. Soc. 74, 358-366, 1953. I can give you three lines of code for which you will not be able to formally prove whether they do what they should; nevertheless it is easy to write unit test and employ TDD in an immensely practical way for the same code. Now, regarding theorem provers---sorry but your statement is quite funny: the track record of theorem provers is poor to say the least. They play absolutely no role for any kind of non-trivial code as all interesting cases are totally intractable, thus being unable to deliver what they promise. But the biggest problem of all: theoretically proving properties of code is far less interesting than finding the correct specification to begin with. The problems that really matter arise because of incorrect formal specifications and misunderstandings between those people who write the specification and those people that want their problems solved. TDD actually is on the very practical side for those 99.9999% of developers and their customers---they could easily employ it if they only would, and the software industry would tremendously benefit from more programmers doing so.
The problem with people like Jeff L. is that they make a living writing TDD books and giving TDD courses. Only a fool would listen to the advice of a man whose money flowed from what he was advising on. When Jeff L speaks of TDD, you cannot tell lies from truth. When he finds something else to earn money from, suddenly he'll go all neutral on TDD and won't write any more comments on TDD commenters.