In addition to the
article recently mentioned on Habré that the full 100% code coverage of unit tests is almost always not cost-effective, since it’s just
too lazy to write all this. ... it requires unreasonable expenses of working time and increases the cost of supporting the code, today I would like to present to the public reflections on this subject Steve Sanderson (
Steve Sanderson ), author of the books
Pro ASP.NET MVC and
Pro ASP.NET MVC V2 .
Introduction
I have been writing unit tests for 3 years now and I have been doing TDD professionally for a year now. And all this time I again and again notice such a thing: for some types of code, unit tests are written easily and naturally, significantly improving the quality of the code, while for others they require a lot of effort, they do not help in eliminating flaws, but rather the opposite - become only a barrier when trying to refactor or improve.
And this is not surprising, since practically all well-established approaches in different areas of life are in fact effective only in certain circumstances, and lose their advantages in other conditions. So here: many developers will agree that it is not always in the writing of unit tests that is understood.
So the purpose of writing this note:
- understand what actually determines the value of unit tests for this particular part of the code;
- show the inconsistency of the common opinion about the need for 100% coverage and the mandatory writing of tests before the implementation of each functional block.
Test Benefits
The whole list of advantages of having unit tests can be reduced to two main ones. They allow:
- design the code directly while writing it;
- make sure the implementation really works as intended.
But there are questions: why do we need an extra system of design and testing, does not the code itself carry information about the device and the behavior of the application? and if the tests do not provide fundamentally new information, how do they confirm the correctness of the designed system? what about the “principle of non-repeatability” (
DRY )?
')
For me personally, if at the first thought of a task, the code for its implementation does not become obvious, which means you have to sit and think about it for writing, then additional help (for example, in the form of unit tests), allowing you to make sure that everything will work correctly, would be very helpful. For example, if you develop a business rule system or analyze a complex hierarchical expression, you will not immediately be able to trace all possible branches of code behavior in your mind. In such cases, unit tests would be extremely valuable.
And vice versa: if the code is simple and obvious, and at first glance it is immediately clear what it does, then the benefits of the advantages that unit tests possess are reduced to zero. For example, you write a method that receives the current date and the amount of free disk space, and then transfers this data to another method. In this case, your code speaks for itself, additional unit tests simply have nothing to add.
So, the benefits of unit tests directly depend on the degree of code confusion.
Price testing
Among the factors affecting the cost of the product are the following:
- time spent writing tests;
- time spent on correcting and reworking tests after refactoring the code or making other changes to it;
- fear of making any improvements in the code because of the expectation that because of them some tests will be ridiculed and all of them will have to be rewritten.
Of course, the cost of supporting the test system can be reduced by following
various recommendations , but it can still remain relatively large.
According to my observations, the total cost of unit testing of a certain piece of code is very strongly correlated with the number of dependencies existing in it on the remaining parts of the code. You will ask why?
Initial creation. If the method has no dependencies on other blocks and simply works as a normal function that takes one parameter, the unit test will simply be a list of matches between the input and output data. But if a method takes many parameters and interacts with many external services through class properties, you will have to make a bunch of fake (mock) objects. But the cost of this work is small compared to the next item.
Support. It has been established that the more dependencies exist in a code block, the more often this code is forced to undergo changes (this is exactly how the code instability is determined). And the reason is clear: for a given period of time, for each of these dependencies, there is a probability of changing its signature or behavior, which will result in the need to update the code and the corresponding tests.
Please note that these problems also apply to the situation when you are using
IoC (
DI ), working with clean interfaces.
So, the cost of unit tests directly depends on the number of dependencies in the code section.
Graphic representation of the cost and benefits of tests
This intentionally simplified diagram shows 4 types of code:
- Complicated code with a small amount of dependencies (top left section). Typically, these are self-contained algorithms that describe business rules or implement expression parsing. This type of code is cheap and easy to test, and therefore most preferred for unit testing.
- A simple code with a bunch of dependencies (the section at the bottom right). This section is signed as “Coordinator”, because this type of code is intended to link and organize interaction between other blocks of code. It is unprofitable to test such code: it will be expensive to write tests, and a miser is of practical use. Working time can be spent much more efficiently.
- Complicated code with a large number of dependencies (section on the right above). Writing tests for such code is quite expensive, and not writing is too risky. As a rule, the output can be its division into two parts: a piece that incorporates a complex logic (algorithm), and a piece that has concentrated external dependencies (the coordinator).
- An ordinary mediocre code with few dependencies (the area on the bottom left). You should not worry about the code of this type. In terms of economic benefits, it does not matter whether it will be tested or not.
Finally practice. So what about the ASP.NET MVC?
In ASP.NET MVC, at first glance, the logic of an application is easiest to place in controllers. And while continuing to push business rules into the controller, the latter becomes too cumbersome: it accumulates complex logic, and at the same time remains quite costly to test due to dependence on many objects. The horses, people, or rather the tasks of different application layers (the so-called
“fat controller” antipattern) mixed up in a bunch.
In order to avoid such confusion, independent parts of the application logic must be factored (I apologize for the expression) into the model-level classes. Then from the rest you can separate the parts that are still not consistent with the true purpose of the clean controller, and stuff them by ActionFilters, your own ModelBinders and ActionResults.
The more we structure our controllers in this way, the simpler, cleaner, more beautiful they become, ultimately degenerating into pure water the coordinators who control the interactions between other layers of the application, and without any additional logic of their own, grow into a coherent system. In other words, the better the controllers are structured, the stronger they are to the lower right part of the diagram, depriving them of any sense of testing.
The purpose of the controllers is to be just a meeting place for various APIs of various services. The code for such a controller is easy to read and binds together many dependencies. I came to the conclusion that, from the point of view of economic benefits, my work is much more efficient if, instead of unit-testing controllers, I devote time to refactoring and writing integration tests.
Conclusion
In case someone could misinterpret my words: in fact, I am not against unit testing or TDD. My main points are:
- judging by my own experience, the productivity of my work over a long period using TDD is higher only for those types of code for which it (ie, TDD) is economically beneficial, for a complex code with a small number of dependencies (algorithms or self-sufficient business logics);
- Sometimes I deliberately divide the code into algorithmic and coordinating parts, so that the first can be quite simply subject to unit testing, and the second becomes so clear and understandable that it does not need unit tests; A typical example is the removal of business logic from controllers;
- I am increasingly aware of the practical value of integration tests; for web applications, this usually involves the use of some browser automation tools (such as Selenium RC or WatiN ); Naturally, this does not cancel unit testing, but I would prefer to spend an hour writing an integration test to make sure that the whole system works smoothly than to kill this hour writing unit tests for simple code, whose behavior is obvious to me at first glance , and which is still likely to be changed as soon as the underlying APIs change.
All of the above is only a description of my observations, which may well not coincide with yours.
Original article
Perhaps it remains only to recall the
words of Scott Bellver (Scott Bellware): "TDD is not about testing, it's all about design."