Being a fan of TDD, I have heard and read a lot about the benefits of test-driving software and creating tests in general – as well as about the drawbacks. While the most widely mentioned issue with tests is false confidence that they might give developers, there are also other major problems associated with this popular approach to software development. Most of these come from misusing TDD and because I have mastered the skill of doing things wrong I would like to describe some of the potentially harmful results of test-driving programs.
One problem that I faced is that tests grab developer’s attention. Tests can be easy to write, are known as positive and rewarding activity and support one with some confidence, so in many cases, it is a pleasure to create them, especially when the tested components are small and well-designed. In case they are not, composing test cases becomes difficult and makes one feel that since the task is hard, it must be important as well. Moreover, creating a test suite for a complex piece of software is a challenging task and we, programmers, do like challenges. On the other side, when we test-drive a component, tests allow us to almost discover it as though it is already created and we just keep unveiling it piece after piece. Sometimes, it really looks like this – and it is amazing! In reality, however, we are the ones who create and discover the thing at the same time, hence the exploration might last as long as our imagination allows – that is infinitely long. What I want to say here is that it is extremely easy to fall into hacking tests for the sake of tests themselves or, more precisely, to test much more than it is required right at the moment. While tests are not hamburgers and one may believe there can’t be too much of them, our time and powers are limited, so when utilizing these resources we should do it carefully: our customers may feel good if the product is backed by an extensive test suite, but they will hardly pay for tests alone. Personally, I sometimes find myself writing new tests when taking the next step along development path requires me to make difficult design decisions and I feel afraid of doing this. In such circumstances, the idea that the existing code is not tested well enough comes as a remedy that seemingly frees me from the responsibility for creating new pieces of the system. Whenever this happens, it is crucial to push yourself to advancing the product instead of cowardly trying to cover with tests something, whose existence is justified only by the tests themselves.
Now to hamburgers. How do you think, can software really get fat, sorry, overweight? Every experienced developer shouts ‘Yes!’ now – I almost hear it. Like in case of people, this may be caused by unhealthy food, our problem can come from bad code. Now, try to imagine how difficult it would be for you to move if you are, say, 150 or 200 kg person? Or rather, how do you think, will it be easy to learn to drive if you have difficulties with pulling your entire body into a car (closing the door is not absolutely required)? The same way if a piece of software is written badly – is ‘fat’ – it won’t be easy to add new features to it or even make the old ones work correctly. You will now ask me, what does this have to do with tests? Aren’t tests a kind of gym-training for programs? Well, while it may seem so, I believe they are more like healthy food. Eating proper things is good for your body and health, but eating too much of good things will lead to the same problems as fast food does. Tests serve to encode assumptions, to specify how your components depend on what. Making dependencies explicit is great, but explicit dependencies are still dependencies. This said, when writing tests you set up relations which make future changes difficult – no matter whether these are harmful or not. The above issue becomes very severe when you test implementation instead of interfaces, but even avoiding this won’t save you from pain related to altering an interface of some component, not to mention the changes in the design of the entire system. I have written a post about the problems caused by the decision to change a signature of a constructor and showed some recipes to make the task easier, although the situation described there is just a tiny, toy-like example of how even well-written test suite can resist the progress of a software system. Too many tests unfortunately can’t make your code more reliable and documented at no cost – they also make software inert, ‘fat’.
As though that’s not enough, there are also additional costs introduced by investment into testing. Particularly, when you attempt to create unit tests for a C# class and place them in a separate project (obviously, a good idea), you are forced to make the tested component
public in the C# sense, so that tests can reach to it. That inevitably leads to expanding the interface, opening a larger than required part of your system to the world. Even if one can deal with this by means of extra tools and more complex build process, that's an additional portion of work, which I find hard to justify. Furthermore, while the frameworks and tools that we use to create tests struggle hard to make the process easy and avoid influencing the production code in any way, sometimes they can fail, for example implicitly making certain design decisions more favorable than the other, better ones or simply making us write a bit more code than we need, so that tests do impact our product more than we want.
I don’t write this to restrain one from test-driving software. Instead, these ideas lead me to a banal conclusion that when choosing where to allocate available resources, we have to do this carefully. Tests give us the opportunity to write better code, refine interfaces, avoid many mistakes, better understand what we want to achieve and at the same time document our systems. Still even with all these benefits, there are risks associated with test-driving software and one should keep this in mind, so that no harm is done to a system by testing too much, introducing useless features or opening details of implementation to users.