Why you should (not) use Test-Driven Development

Background

Believe it or not, every programmer is a little bit different. We all have our own preferences, strengths, and weaknesses… It wasn’t until last year that I tried to spend a little time to figure out where I fall in these categories. After getting several years of programming under my belt, I found that my preference was greenfield (all new everything), my strength was a balance of quality and speed, and my weakness was testing. [Note: conducting regular reflections like this is important because it helps us see opportunities that we might otherwise overlook] Towards the beginning of 2013 I was assigned to a new project that gave me a good chance to work on this weakness. So:

Preference – greenfield; Strength – balance of quality and speed; Weakness – testing;

This weakness shouldn’t surprise too many folks in the software industry. What computer science programs do you know of that hold testing in high regard? The truth is that testing is a second class citizen in curricula at best – and for good reason. It can be hard enough to grasp the fundamentals of writing readable code while in school, so taking that momentum into writing maintainable code is even more difficult. For me, concepts of maintainable code didn’t quite click until I got on a few big projects my senior year in school – when I also began my mastery in the art of refactoring (by necessity).

Some code ninjas would say that all this progress was leading me directly to test-driven development (TDD), a method regarded by many as the pinnacle of code-slinging. That is the truth. It lead me smack-dab into TDD, and I (mostly) refused.

Concept: Unit Test

TDD is built upon the idea of testing, specifically unit testing. We can start by defining a unit test: code that tests code. Take this function for example:

It’s a good idea to write unit tests, because the result is a runnable test that says either a) Add works correctly, or b) Add does not work correctly:

Wait a second… We just wrote five lines of code to test one line? Yes, we did – and for good reason. This example portrays the what of a unit test, not the why. Here are a few reasons that I can think of off the top of my head:

  1. As our code base becomes larger, we can forget exactly what each function is supposed to do. Ever wrote a function called SorryAboutThis()?
  2. As we refactor and improve our code, we can eliminate code that is important. Ever tried to maintain a function called SorryAboutThis()?
  3. As other developers inevitably edit our code, they can unintentionally alter behavior. Ever inherited a function called SorryAboutThis()?

If you’ve ever been on a project that had more than 1K lines of code, more than three developers, or a timeline of more than a few months, then you surely know the benefits of unit testing. It is quite normal for a project to amass more test code than code which is being tested (code-code?). Bottom-line: write tests or your life will be hell.

Concept: Red-Green-Refactor

So now that we have unit tests under our belt we can just write ’em, eh? I would be inclined to tell you: yes, go for it and figure out what works for you. But others would most likely first tell you: no, follow TDD’s concept of red-green-refactor. This concept is all about the procedure used when writing code and it’s simpler than you might imagine… Never write code unless you have a failing test, i.e. red. At which point you write the code needed to make the test pass, i.e. green. Followed finally by your chance to do any improvements to code, i.e. refactor.

For junior developers this process is completely backwards – how do I write tests for code that doesn’t exist? It’s actually pretty simple. From the code samples above, we would write the test for Add() which would initially fail. After that, we would write Add() with the bare minimum needed to make the test pass. We would end by making any improvements needed to Add(), ensuring that tests still pass.

This process is repeated while (true)… sorry, I couldn’t resist. [Note: you can also follow a variant of this process. I prefer red-red-…….-red-green-refactor, wherein you write several failing tests, make them pass, then refactor.]

The Good

In my opinion, TDD brings a one spectacular practice to the discussion:

it forces developers to think about testing early and often

So often, testing falls to the wayside and the product suffers for all the reasons listed above. All of those reasons boil down to maintainability! Forcing developers to test early and often in an environment that fosters education is the most beneficial practice I’ve seen for junior developers (to date).

In my current project (mentioned briefly in the background), I talked about my chance to employ TDD with my team in hopes of reaping the benefits. It’s been easy to see the improvement that our junior developers made on this project. So often, I see code like this:

… turn into code like this:

Take your pick. We’ve wrote some of the coolest code for this project and it’s extremely maintainable due in part to TDD (and other portions of our process which include regular code reviews, pair programming, etc.).

The Bad

Even with the great practices that TDD helps our team instill, I prefer to not use it because it’s simpler not to. At some point, TDD and other great practices reach an education plateau. You gain all the knowledge it offers and it becomes nothing more than a process.

If you write maintainable code that follows the SOLID principles and you test often, then TDD offers you absolutely nothing outside of being a process to follow.

Like I mentioned earlier in the post, all developers are a little different and we each have our own preferences. I prefer to not use TDD because it prohibits my ability to design future code. The process handcuffs developers from thinking far ahead in favor of only letting them write code in small iterations. This leads to TONS of refactoring when you realize that you should’ve wrote something differently.

Conclusion

Overall, I think TDD is a useful learning tool – but that’s it! I am very happy that I gave it a solid chance on a new project. The team had a very successful run with TDD, but I don’t enjoy the process. I challenge you to reflect on how you write code and how you might experiment to improve.

What are your preferences, strengths, and weaknesses? What do you think about TDD? What are your experiences with TDD?

Posted in Professional Tagged with: , , , ,
8 comments on “Why you should (not) use Test-Driven Development
  1. Derrick says:

    For me, another benefit of TDD is that I’ve verified I’m not going to be making a complete fool of myself when I show my code to the team. (Also decreasing the chance of a prod bug being my fault) That and the value of knowing you are not breaking past features when you have to refactor code with awhile ago. Also it’s a pain when you are writing good unit tests and others on the team are not.

    • brhlavinka says:

      Derrick – all the benefits you mention could be attributed to unit testing, not necessarily the process of TDD. My main point here is that the process is useful for learning. Once you learn how to write good tests (and the obvious reasons for doing so), it is completely fine to write them retrospectively. As mentioned, I dislike TDD because I feel that it is restricting.

  2. Chris Aikens says:

    In my experience unit testing is a practice that reveals code smells or outright flaws. Have you ever tried writing tests for someone else’s code and couldn’t because it didn’t follow expected/logical paths? TDD could have prevented some of that because it forces more accountability on the programmer.

    I find that TDD gets the shaft in hyper-agile projects where features are needed now and not after someone writes a block of tests… TDD requires a deliberate patience that some clients may not have. “Why did we hire you Agile guys if you’re going to spend all day writing tests?”. TDD is more ‘do it right the first time’, where just writing unit tests is more ‘I will do this right eventually’.

    • brhlavinka says:

      Chris – that is a really good point. I can definitely relate to feeling pressure from a client who says ‘get things done, forget about the tests’ … It is part of our job as devs to insist on showing the value in testing.

  3. Daniel Herrin says:

    Brett-

    Wow. What a great blog post. I want to say thank you first. I enjoyed reading your analysis after working with technology and process.

    It is an interesting point of view on TDD. A couple of thoughts for you (simply playing devil’s advocate): Risk of building code that is not used, and Risk of unit testing coverage.

    You mentioned that TDD constrains you to writing code for the present problem and that if you would/could think about the larger you might write it differently and avoid refactoring costs from the TDD. The flip side of this argument is that TDD (or similar) keeps the developer focused on the current issue and not attempting to solve world hunger with a much bigger solution. The down side of alway thinking about the long term is that 1) the long term may never actually come to pass (so you have wasted code that will never be exercised, 2) your development velocity for the first iterations is much much slower because you are writing code for the later iterations as well, 3) at some point the problem becomes unattainable because you are considering the bigger picture. Maybe said more simply, TDD (or similar) can help a programmer save themselves a lot of grief.

    You also mention writing unit tests in respective. I completely agree with you if you, your team, and your client has the discipline to follow through and write them. You even mention yourself about the “we will come back to that”. You could inadvertently be building technical debt that has to be paid at some point. It is such a dangerous slop and so so so easy to slip over. A process of writing tests before hand help mitigate this risk.

    I would also like to see some of your peers views who are big TDD fans weigh in here on this topic.

    I really enjoyed reading it.

    • brhlavinka says:

      Daniel – good points on keeping the developer focused. Obviously a concern early in the project as we can unnecessarily over-architect. I would also have to agree with your point on technical debt. I’ve regrettably found myself in this position several times. The way our teams have mitigated this in the past is to hold each other to higher standard when ‘closing’ user stories – make sure that tests have been written before signing off.

  4. Jared Thigpen says:

    In general I fall in line with your thinking, Brett. The important thing is to have *a* process that reinforces the importance of test coverage. That might be TDD, which is very rigid on when and how you write the tests, or it might be as simple as a “definition of done” for user stories that is clearly spelled out and enforced within the team.

    I prefer the latter approach because it gives the developer the power to use the process that works best for them. In my last project I mainly wrote unit tests retrospectively, but I built one specific feature that was so complicated and involved that I felt more comfortable writing the tests first so that I had an easy to follow “contract” while implementing the logic.

    I will say, however, that I can see a lot of value in forcing junior developers to implement TDD at least once to really drill into them what good tests and testable code looks like.

    • brhlavinka says:

      Jared – thanks for the read. I’ve gone with this approach in the past with varying levels of success. Seems like it’s always a challenge to keep developers motivated to test!

Leave a Reply

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

*

Subscribe to my Insights via Email

Join 61 other subscribers

%d bloggers like this: