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.
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.