From a technical point of view, unit tests are a very simple tool based on a couple of simple concepts: (1) the class under test, (2) a set of test methods wrapped in a certain class, and (3) a set of methods with which you can verify That the state of the test class corresponds (or does not correspond) to some value.
This is a very simple thing that can dramatically affect the development process as a whole. On the one hand, there is TDD (“test-first approach"), in which tests "drive" not only the coding process, but also the design process (ie, the system design). On the other hand, there are developers with opposite points of view who consider unit tests are a waste of time because they bring no value to the user.
I respect and understand the first category of developers, although for me personally the test-first approach is not so convenient as to apply it in daily activities. I used to think about the design of the subsystem with a pencil in my hands and it’s too tiring for me to add a new method only after adding a test. But here, in fact, it's not about when I prefer to write unit tests, but what these tests mean to me.
')
The common opinion about unit tests comes down to the fact that they are intended primarily to improve the quality of the code. Another useful feature is that the code can be refactored painlessly, because the tests will ensure that the original behavior has not changed.
It’s hard to argue with these arguments, but these are far from the only (and from my point of view far from main) advantages of unit tests.
For me, unit tests are, first of all, a “litmus test” of good design. Good design has such remarkable characteristics as weak connectivity and clear relationships between classes and their clients. If it is difficult to write a unit test, it is impossible or it turns out to be too big and confusing, then it often speaks of problems not so much in the test itself as in the design of the code under test.
Property 1. Unit tests have a positive effect on modularity and system design.When designing a system, I often ask myself the question: “Ok, this is a great idea, but how are we going to test this business?”. If a class depends on many other classes, directly uses external resources, or takes on too much responsibility, then testing such a class will be incredibly difficult. I'm not a big fan of design for the sake of design or design just for testability. But as practice shows, so far it’s not necessary: ​​a good system design is weak enough to cover most of the key parts of the system with tests (you can read more about unit tests on design and architecture in the article
“Ideal architecture” ).
The most interesting thing is that unit tests affect not only the design of classes, but also the implementation of methods. Methods with unclear or complex responsibilities are not only difficult to read, but also difficult to test. “Pure methods” (ie methods without side effects) are ideal from the point of view of unit testing, because they do not depend on anything other than their arguments and do nothing else but to return the result.
But even if you do not climb into the “functional” jungle, the need for unit testing can, for example, help determine the “tactics” of exception handling: should the method throw an exception or can it be “captured” right on the spot. If the method cannot do its work and does not say anything to the calling code, then it will be difficult to test its performance using the unit test, which means that this method should somehow inform its customers about the failure.
Property 2. Unit tests are an excellent source for system specifications.In one of the podcasts, Kent Beck, the father of JUnit, TDD, and extreme programming, gave the following characteristic of a unit test: each unit test should tell a story about the class being tested. A unit test is a useful source of specification (formal or informal), and not just a collection of Asserts. Often, it is the reading of unit tests that can help understand the boundary conditions and business rules applicable to a class or subsystem.
One unpleasant consequence follows from this property of unit tests: the quality of tests needs to be given no less attention than production. This means that the test should be easy to read and should be easy to maintain, otherwise its life will end after the first fall. A programmer who will have to correct this test will find it easier to tear it down completely, rather than figure out how this miracle works. In addition, tests can be used as a specification only if they represent a “higher level of abstraction” compared with the business logic code. No one will use tests as a specification, if it is easier to understand the code itself than a unit test.
Property 3. Unit tests allow you to reuse the effort spent by the programmer.We all know that the joyful and beloved stage of development is much shorter than the despondency that begins when the system is accompanied. This is a button accordion, but the fact that everyone knows about it does not make our code easier to maintain. Apart from the fact that unit tests have a positive effect on the design and are a source of specification, they also contain the experience spent by the developer in realizing a certain possibility.
Firstly, unit tests make it possible to better understand what the programmer was thinking about, which boundary conditions he checked and which not. Secondly, during the implementation, the programmer plunges much deeper into the context of the problem being solved, which can be expressed in additional comments inside the tests, and his spent efforts can be reused (because each subsequent programmer will need less time to estimate the depth of all depths).
Property 4. Unit tests save our time.At first glance it may seem that this property of unit tests is a complete absurdity, since the development of tests requires additional time that could be spent on developing useful features.
Of course, there is a reasonable component in this, especially if tests are written for the sake of tests, or they are so miserable that the cost of maintaining the system is tripled. I agree that bad tests do more harm than good, but if you approach them with the same responsibility and reasonable pragmatism as with the rest of the code, you will feel the benefits of them.
In addition to the obvious advantages in the long run (it is still easier to accompany the code, with normal tests), there are advantages and more “myopic" ones. In a large system, it can take a lot of time to test a new opportunity just to raise the working environment, start up the five services, the client application, then scream through ten screens to understand that the text box you just added behaves incorrectly. It sounds implausible, but it means that either you are lucky with your projects, or, on the contrary, I am not lucky with mine. Anyway, most unit tests are the fastest way to start or debug certain classes.
ConclusionI am not an ardent fan of unit tests, I am not a TDD fan, and I don’t support 100% coverage. If only the author can understand the tests, and in order to get a decent coverage, so many abstractions were stuck, that here the devil breaks his leg, then you and I are not on the way. For me, tests are, first of all, the opportunity to look at the modularity of the system from a different angle, to consider classes from the point of view of their inputs and outputs, analyze the boundary conditions and understand what a class should do and what not. Like any tool, it is easier to use it incorrectly, and for proper use, experience and common sense are needed; Tests are a good thing, just need to learn how to cook them.
Additional links1.
SERadio Episode 167: The History of the Universe and the Future of Testing with Kent Beck2.
Kent Beck. Development testingThese are two simply amazing podcasts featuring Kent Beck, in which he talks about the development testing culture, o JUnit, Continuous Testing, and much more. Highly recommend!