How Unit Testing Reduces Programmer Costs
Many people think that developers spend 80% of their time writing code. But this is a misconception. There are a huge number of related tasks that collectively take more time than code.
Let's take a closer look at what a developer's job consists of:
Analysis of task requirements
It includes proofreading the technical specifications, design, team grooming with the study of new functionality. It is very important not to make a mistake at this stage, so it can be given as much time as the direct development of new functionality.
Search for a technical solution
It is critical to focus on this stage to find the right solution. Mistakes made here will be very costly.
Development of a technical solution (that same writing of code)
Here, the programmer, based on the technical specifications and the selected technical solution, writes code to implement new functionality, or support or change the old one.Preparing artifacts
After the developer has written new code, he uploads it to the branch and creates a Merge Request. As a rule, the necessary jobs are run on CI, which check the code formatting in the project, the status of the corresponding task, filling in the necessary fields and other things that need to be corrected before sending the code for review. Here, if necessary, conflicts with the common code base are resolved.
CodeReview
At this stage, several developers (usually two or more) check the code you have written for errors, the correctness of the technical solution chosen, and the fulfillment of other requirements.
Preparing for testing
Once the developer's code has been checked, they have fixed all the detected errors and have received enough permissions to merge the code, you can move on to preparing the new functionality for testing. This may include, for example, assembling an apk/ipa file in the case of mobile development and delivering it to the tester.
Functionality support
After delivering new functionality to the tester, the developer's work does not end. At this stage, he continues to support the new functionality, discussing with the tester the paths for testing, the functionality affected, as well as issues of interpreting the technical specifications.
Preparing for release
Once the functionality is fully tested and all errors are fixed, release preparation begins, during which all tasks are collected into one common assembly, the application version is increased, and other infrastructure tasks are solved.
Regression testing
After assembly, the release goes to regression testing, where all the functionality of the application is checked, since the new version could break the old one.
Delivery to users
The final stage of the process is release, when the new functionality is rolled out to users.
Functional support
This stage is similar to functionality support, except that support and maintenance is provided not to testers, but to end users.
Depending on the specifics of the company and the employee's position, the processes may differ: as a rule, by the appearance of additional stages. So, if the work is related to critical infrastructure, an information security audit will be added, and if it is related to UI, then UI testing will be connected.
As we can see, writing code itself is just one of at least 11 engineering tasks of a developer.
The idea that this stage takes up a small portion of the overall development time is supported by research. A survey conducted by Tidelift and The New Stack found that writing new code and maintaining old code accounts for about 33% of the work time. The rest of the time is distributed as follows:
![](https://habrastorage.org/getpro/habr/upload_files/7b1/2a7/a73/7b12a7a730a85f42a2c52a4535199979.png)
Motivation of developers
Developers generally don't like writing unit tests: they see this activity as redundant. Let's take a closer look at how writing unit tests will affect the overall developer workload.
First of all, it is carried out task requirements analysisThis is an important step because if it is not done well, a requirement in the technical specifications may be missed or misinterpreted, and subsequently the entire work will have to be done again.
Unit testing easily solves this problem, because it is at this stage that test cases for the upcoming task begin to be written. To do this, you need to work out all the scenarios in detail (positive, negative and destructive), determine their boundary conditions, create decision tables, draw a graph of states and transitions, etc.
All these implementation details can be consistently transferred to unit tests, confirming that the implemented functionality works correctly. This work will allow you to consider in detail each aspect of the task being worked on, which will reduce the likelihood of errors at this stage and the need to correct them in the future.
Stage search for a technical solution is largely related to reading other people's code and documentation. The code is not always written clearly, and the documentation may be missing, out of date, not reflect implementation details, or written in a business language (not always fully accessible to the developer).
Unit tests, in turn, can be interpreted as clear and simple documentation: it is written in the developer's language, displays all the nuances of the functionality that were recorded at the previous stage of analyzing the requirements for the task. The developer can immediately understand how a particular class works using a short example, and at the same time be 100% sure that the tests are relevant, because otherwise they would return an error.
Stage development of technical solutions is directly related to writing a new solution, and it is within its framework that unit tests for existing functionality are written.
The acceleration of development at this stage is due to the following factors:
Quickly check the solution for compliance with requirements. Often, tasks can be quite large, and when developing them, you will need to constantly check the requirements to check whether the functionality meets them. In the case of development without unit testing, you will need to constantly refer to the documentation or try to keep all the requirements in mind, which is quite time-consuming and labor-intensive. Also, in this situation, it is easy to overlook or forget something, creating a technical debt with future corrections and alterations. The practice of unit testing will reduce this problem to zero.
Reducing feedback time. As a rule, a lot of time may pass between writing the code and the moment a defect is discovered. The developer may have time to forget the requirements for the task, as well as the specifics of its technical implementation. As a result, he will have to spend time studying the documentation and searching for the necessary conditions again. The practice of unit testing changes the situation radically: the developer will instantly learn about the created defect.
Improving code quality. The practice of creating unit tests imposes positive restrictions on writing code. We check not only the functional requirements for the task, but also the correctness of its implementation from an architectural point of view. In such a paradigm, the developer is forced to constantly ask questions: is the code sufficiently isolated, are all dependencies set from the outside, etc. All this ultimately improves the quality of the code in the project, and therefore reduces the time for subsequent revisions and corrections.
Dilution of tasks. Writing unit tests dilutes the developer's routine. So, if he works on a large and complex task for a long time, over time he may begin to get tired of it, motivation and productivity will gradually decrease. Writing unit tests will help to distract himself from this task for a while, rest a little, and also raise motivation.
R. Martin writes about this in his book “Clean Code”. Discussing the formation of architecture, he identifies several rules, among which is the most important: “Surprisingly, following a simple and obvious rule that states that tests must be written for the system and constantly executed, affects the system's compliance with the most important criteria of object-oriented programming: eliminating rigid bindings and increasing coherence. Writing tests improves the system's architecture.”
Stage Code review thanks to unit testing, it will be greatly simplified. And both from the side of the developer whose code is being checked, and from the side of the person checking it.
It will be easier for the reviewer to understand the new functionality, since he will be able to use the written tests as documentation in a familiar technical language. It will be immediately clear which cases the developer took into account when creating the new functionality and which ones he might have missed. The developer being reviewed will receive fewer questions about the specifics of the functionality implementation, as well as the requirements for it. The number of architectural errors and comments that require significant time for revision will decrease. The number of code review iterations will also decrease, since unit tests will check many aspects of the task implementation even before sending it to Code Review.
At the next stages (delivering functionality to the tester, testing, regression, delivering functionality to the user), unit tests will make life easier for the developer in a similar way: most errors will have already been identified by this point, so you will perform these stages fewer times. Below I explain in detail how this happens.
The price of error
The cost of an error is the time it takes to correct it, as well as the human resources that will be involved.
The later we detect an error, the more difficult it is to fix it, since more functionality will be affected. So, if we made an architectural error at the stage of searching for a technical solution, and noticed it only during regression, then when fixing it, we will have to go through all the stages again from a new search for a new architectural solution through development, unit testing, Code Review, manual testing, release preparation to new regression testing, as shown in the figure below. Too many reworks due to an error detected too late.
![](https://habrastorage.org/getpro/habr/upload_files/be7/dd6/b86/be7dd6b862a952fd638a65bd7ef3b134.png)
The later an error is noticed, the more people are involved in fixing it. For example, at the Code Review stage, three people fix an error: the code author and two reviewers. An error noticed at the testing stage will require four people: a tester will be added. When detecting an error using unit tests, only one human resource is used – the developer himself.
Don't forget about the time-consuming costs of context switching. A developer who did a complex task two months ago won't be able to switch to it instantly and make a quick revision: he'll need to study the context and figure out how the functionality works.
Based on the above, we can conclude: the earlier an error is noticed, the less it costs to fix it. S. McConnell writes about this in his book “Code Complete”, where he says that the average cost of fixing an error detected at the testing stage is ten times higher than that of an error detected at the development stage.
And now the numbers
The benefits of implementing unit tests are difficult to express in numbers. This is due to the fact that the number of errors that occur is influenced by:
Project size and complexity: In large and complex projects, unit tests can catch more bugs than in small and simple ones.
Code quality: If the code is written poorly and without testing, then unit tests will reveal more errors.
Code coverage level of tests: the more code is covered by tests, the more bugs they can detect.
The effectiveness of the tests themselves: Well-written tests will be able to detect more bugs than poorly written ones.
Isolation of unit tests: non-isolated tests depend on the code or environment they test and in which they are tested. Non-isolated tests have to be rewritten with each change in the code being tested. Repeatedly rewriting unit tests significantly reduces their quality and results.
Despite the difficulties, let's try to estimate the impact of unit tests in numbers using our project as an example. To reduce the impact of the above factors, we took similar tasks of one developer and compared the number of errors in tasks fully covered by tests and those with little coverage.
![](https://habrastorage.org/getpro/habr/upload_files/ad1/b14/d27/ad1b14d274ee675370d89e724e90faff.png)
To sum it all up, the average number of bugs found in tasks that are poorly covered by tests is 4.5 versus 0.5 in tasks that are well covered by tests.
Based on Jira data, the minimum time to fix one bug is 4-6 hours. Add to this the time spent on Code Review, delivery of the build to the tester – and you get a whole working day spent on fixing just one defect.
It will take one to two days to write unit tests for a task of this size. It turns out that for tasks without unit tests on average it takes 4 days longerthan for similar tasks, but covered by tests.
conclusions
In this article, I have shown the benefits of unit testing in software development using a specific example of an average software engineer.
Despite the increase in time spent writing unit tests, the practice of unit testing reduces the overall time spent developing new functionality because:
allows you to detect and correct errors at the early stages of development, which significantly reduces the time and resource costs of eliminating them;
promotes improved code quality by creating positive constraints for developers;
simplifies the process of integration and documentation of code, encourages the developer to make changes and refactor.
In general, unit testing makes the development process more efficient and manageable. In our case, unit testing reduces the development time of one average task by about four days.