Test-driven development

In an earlier article I discussed the merits of unit tests. In essence, unit tests are pieces of code which assert that your production code works as intended. With a sufficient amount of unit tests it is possible to refactor your production code at any time, because the safety net of knowing when you broke something gives you courage.

Typically, programmers write code and test it afterwards. Code which is not easily unit testable is not tested or integration tested. Execution paths are overlooked, boundary cases get ignored. There is not much safety for future changes, because there is no machine-executable specification of how less well-known regions of the code are supposed to behave. Enter test-driven development (TDD).

The essence of test-driven development

TDD has been around for a few years. It is the state-of-the-art programming style when it comes to work efficiency and software maintainabilty. The basic idea is to change the order in which production code and test code are written:

  • Write a small amount of test code which your current production code cannot pass. For the case of compiled languages such as C++, this includes the possibility that your test code cannot even compile yet.
  • Make sure your test fails. This step is important since it tests your test; you gain confidence that your test can detect errors in your program code.
  • Add just enough production code to pass the test you just wrote and all previous tests.
  • Let all your tests run again. Make sure all of them pass even though you know they will.
  • Take a breath. Look at your both test and production code. Identify code and data duplication and remove it. Apply other refactorings as you see fit.
  • Yet again, check all tests pass.

Repeat this cycle until you are done.

Learning TDD

Making yourself familiar with TDD in C++ is surprisingly simple. All you need is a compiler, a unit test framework such as CppUnit, and a simple task to start practicing. I recommend to pick something not related to your work to get yourself started. For example, write a function which converts arabic numbers to Roman numerals. The task should be easy so that you can focus on learning TDD, but it should not be trivial.

When you start writing tests, think about the smallest possible steps. Here are some things you could do:

  • Include a header file in your test which shall hold your production code once you are done. The compiler will complain about not being able to find the file in question. Fix this test by creating an empty header file.
  • Use a non-existing class or function in your test. The compiler will complain about the unknown symbol. Add the declaration to your header file. The linker will complain about the symbol not being defined anywhere. Add a source file which holds the definition of your symbol.
  • Use a non-existent namespace in your test. Again, the compiler cannot do its job unless you add the namespace to your production code.
  • Write simple tests which assert the return value of a function. Fake your implementation by returning the very same value. Write another test for the same function. Improve on your fake implementation to let both tests pass at the same time.

For a more exhaustive list of patterns and detailed case studies, you might want to read Kent Beck’s book Test-Driven Development by Example.

Really, is TDD that slow?

From above items you might infer that TDD is a ridiculously slow technique and this article is not worth the bytes it is encoded in. I might be biased regarding the latter conclusion, but TDD is not for working slowly. TDD empowers you to work as fast as you currently can:

If you are working on a simple problem and you have had a good night’s sleep, make larger steps. Never forget to write tests before production code, though, and never write production code which is not covered by your current tests. If you are tired and you are working on a difficult problem, decrease your step size until you feel confident. Slowing down is not a sign of weakness or lack of skill. Everybody can step on the gas. The best race drivers, though, know exactly when to brake.

Costs of working test-driven

Some programmers complain that writing tests takes time which would be better invested in implementing new features. This does not imply that these programmers do not test their code. They do, but they do it on-the-fly: A little cout here, another “safety” branch there… TDD makes the required testing effort explicit. Never kill the messenger.

With TDD it is possible to achieve 100% code coverage, i.e., each line, each branch of your production code is tested. Besides well-tested production code, you also have a set of tests which illustrate how to use your code. At a later stage, you simply need to reevaluate your tests to prove your code works.

In my experience, the best side-effect of TDD is that you design your code to be easily testable. Units become smaller, functions more cohesive, and classes obey the single responsibility principle. In a word, your code becomes cleaner.

Fun and productivity

I guess it is safe to say that most of us have made programming our profession or hobby because we enjoy it. When I first heard of TDD, I could not imagine writing tests being fun. Oh boy was I wrong. With TDD, a sense of immediate progress is your steady companion. It is like playing a good computer game, there is action all the time. More than once I missed my ride back home because I just wanted to write one more test.

For me, TDD is the most enjoyable style of programming. For my employer, it is the most efficient one. Because I write tests before code, I think more about boundaries. Whenever I make a mistake and introduce a bug in my production code, my tests immediately provide feedback and I can fix the issue while my knowledge of the code is still fresh. Months later, I can add new features without breaking anything. I am more productive with TDD than I was without, and I am sure everyone can be.

 

7 Replies to “Test-driven development”

  1. Thanks for this article.
    Have recently started using the BOOST unit testing stuff to build in reliability and spot bugs early. In retrofitting the unit tests a number of bugs were thrown up.
    Had I had to track the bugs down at a later stage of a project; the costs in time and stress would be much greater. In the past I have seen it quoted that if a bug caught at unit testing costs X then if caught at program testing it would cost 6X and at system testing 36X.
    Still find it wearisome to put the test cases together tho!

  2. Thank you for the article.

    In my very limited experience of TDD (so please correct me if I’m wrong) I happen to always associate it with a major problem I’d like to call “Test Sclerotization”. Basically your code gets heavy and extremely set in stone or rigid even at very early stages of development where things might still change very quickly. A change needs changes in your test harness and effectively at least doubles your workload on every change and thus makes you slow. In addition to that case it does not guarantee that every place that uses the particular code under test has been adapted to the newly established/changed specs (assuming unchanged interface, compiler does a better job at this than a test). Regarding logical mistakes, they usually restrict to very specific domains, where our brain starts biting its limits (for example complex recursions or the like, which our brains are not naturally wired for). If you are able to keep true to KISS, you should not run into to many of them. So why incur the additional weight of tests for a 100% of your codebase just for testing the details, that might be undone and changed in a fairly quick pace… is BDD not enough and much more specific to the actual problem of asking the “does the whole system behave as specified” question?
    Is there anything against only relying on sane design, striving for low complexity and a good compiler to get the signatures right, and only testing the system to have a mean to communicate requirements and specifications with the client?

    On a totally separate note, I (hopefully) do understand TDD in high level languages as means to try to have the application scale. IMHO in that case it is primarily about mitigating the lack of static types and thus avoiding type related problems at runtime. Then again it wastes away the primary power of those languages which is its flexibility and thus resulting dev speed… so again at odds but actually a necessary evil?

    This is the first time I actually ask about this… it has always bugged me, but since TDD it is so overwhelmingly lauded, I’ve always questioned the sanity of my perspective on the particular matter… would really appreciate if someone could shed some light into this.

    Best regards,
    ~Arlias

Leave a Reply

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