Unit testing in complex applications
No developer with sound mind and sober memory while developing complex applications (> 100K LOC, for example) will deny the need to use testing in general and unit tests in particular. This is as true as the fact that every developer will try to exclude meaningless work from the creative process of creating an application. Where is the line that separates necessity from meaninglessness if we talk about unit testing in the context of complex applications? I set out a couple of my thoughts on this under the cut.
Appointment
Unit testing, or unit testing, is a programming process that allows you to check the correctness of individual modules of the source code of a program.
The idea is to write tests for each non-trivial function or method. This allows you to quickly check whether the next change in the code has led to regression, that is, to the appearance of errors in the already tested places of the program, and also facilitates the detection and elimination of such errors.
Everything is as if clear. There are 5 lines of code:
class Calculator {
public function add($a, $b) {
return $a + $b;
}
}There is a unit test for it ( already 10 lines, but this is normal for a unit test, when the number of lines in the test exceeds the number of lines of the tested code ):
class CalculatorTests extends PHPUnit_Framework_TestCase {
private $calculator;
protected function setUp() {
$this->calculator = new Calculator();
}
public function testAdd() {
$result = $this->calculator->add(1, 2);
$this->assertEquals(3, $result);
}
}The test allows you to check the logic of the code and detect an error if something or someone has violated this logic. Since unit tests test code separately from the entire application, they are very simple and very fast, and are able to evaluate the "health" of a significant part of the code in development in a very short time.
Unit tests alone do not guarantee the correct functioning of the entire application, but they are the first, basic stage in the list of tests:

(the picture was taken from the Internet exclusively for its triangular shape and a layered listing of some types of testing; percent numbers and other details are insignificant in the context of the above; )
Trivial triviality
" ... write tests for every non-trivial function or method. "
With code that implements logic according to a given specification, everything is clear. And what to do with code where this logic itself is not? For example, with accessors in DTO-like classes?
The network mind classifies such cases as trivial, despite the non-zero probability of having an error like this in the code:
public function getDateCreated()
{
return $this->_dateUpdated;
}The likelihood of such an error greatly increases with the mass application of the Find & Replace progressive technique in the code, and the desire to use the progressive technique increases with the growth of the project and a more complete immersion in the details of the subject area.
A compromise between meaninglessness and necessity may be accessing accessors when preparing data for testing other, less trivial classes (such as services) that use DTO-like objects, or checking the result after returning through assertes:
$in = new InDto();
$in->setId(4);
$out = $service->callMethod($in);
$this->assertEquals('success', $out->getStatus());Although in this case, the principle of isolation of the tested code from the rest of the application code is violated. Well, he’s a compromise to choose the third of two very good options, not a bad one.
Nontrivial triviality
All object-oriented developers sooner or later came across the abbreviation SOLID ( who did not come across - it 's time ), in which the first letter " S " corresponds to SRP - "the class should have only one duty ." A methodical and consistent application of this principle leads, on the one hand, to simplification of the code of a particular class, and, on the other hand, to an increase in the number of classes and the relationships between them. To overcome the growth problem, a modular approach , multi-level architecture and control inversion are successfully used . In the net, we have a solid profit in the form of " simplifying the code of a separate class", up to such implementations of individual methods:
public function execute($in)
{
$order = $in->getOrder();
$this->_service->saveOrder($order);
$this->_otherSrv->accountOrder($order);
}Testing such a code again balances on the border between senselessness and necessity - in essence, the test boils down to creating stubs / mocks and verifying that the corresponding methods will be called in the appropriate order. Exactly the same effect can be achieved much faster if you make a control copy of the file with the source code and report on testing all deviations of the current code from the control copy.
Dimitar Ginev's colleague recommends dividing the code into two categories of classes ( orchestrator and decision makers ) in such cases and covering only the second category code with tests.
Code coverage
A great metric for evaluating code quality is the% coverage of the code with tests. This percentage can be calculated both for a separate file with the source code, and for the entire code base of the project (for example, Magento 2.1.1 module test coverage ). Coverage of the code makes it possible to visually assess the problem areas in developing the source code and should strive for 100% coverage of meaningful code. Moreover, the more complex the application being developed, the more significant code is in it, and the 100% coverage begins to have more significance. Unit tests are very good candidates for using their results in calculating this metric, again because of their independence (from each other and from the rest of the code that is not tested at the moment) and speed of execution.
Coverage of all code in a project can be brought up to 100% in two ways:
- Create tests for uncovered code
- get uncovered code out of control (for example,
@codeCoverageIgnorein PHPUnit);
The first method implies that tests will also have to be created for each trivial function or method , which increases the meaninglessness. The second method is fraught with skipping tests of non-trivial code, which negatively affects the need.
So where is the balance?
Since the community agrees that there is no need to test trivial functionality, it is quite obvious that the simpler the code or the more ingenious the developers, the less reason to create tests in general and unit tests in particular. Conversely, the more complex the code or the more mediocre the developers, the more reasons. That is, if you are developing a project on 100K lines of code alone, then you can completely do without tests at all, but as soon as another developer (not as brilliant as you) is connected to the project, the need to create tests increases dramatically. And if this developer is also junior, then the tests become vital, because even your genius can save you from the enthusiasm with which junior makes mistakes in your favorite code.
If at the initial stage of development it is completely possible to exclude trivial code (accessors and orchestrators) from unit testing, then the more the project becomes and the more people work on the project, the less trivial code remains in it. In the extreme case, when the code is publicly available (i.e., a pull request can come from some bored parking guard who decided to be a programmer that night), every line of code should be covered with unit tests, even the most trivial one.