SMART DEBUGGING: BEST PRACTICES

Each developer is faced with the need to fix defects/bugs/errors in developed programs. And unfortunately, according to the results of the analysis given in Steve McConnell’s book “Perfect Code”, statistics show that we spend quite a lot of our working time on this. Even one hundred percent coverage with code tests does not guarantee the absence of errors. By reducing the time of debugging, fixing and preventing the occurrence of new defects, we will save time on what we like the most – on solving complex problems. In addition, this will significantly reduce the cost of developing the product.

Tech leads interact a lot with developers on the team: mentoring, consulting, training, pair programming, code review. In the process, it becomes clear: everyone makes mistakes – from a beginner who has just stepped on a career path, to a seasoned developer with many years of experience.

And these observations led us to the formation of a checklist, which helps us as a team to quickly find the problem, as well as prevent the emergence of new ones.

The purpose of this article is to describe a systematic, phased process for identifying and preventing errors.

Stock up on oxygen, we will be immersed in the abyss of bugs.

Causes of Errors

Typically, errors occur when writing or modifying code. It should be understood that 99% of errors are the responsibility of the developer. But with the causes of their occurrence, we will deal further.

Let us highlight the following categories of reasons (by priority of detection complexity):

  • typos and carelessness;

  • lack of understanding of the logic of the code section;

  • ignorance of the intricacies of the development language;

  • false tests;

  • lack of error handling;

  • copying other people’s mistakes.

  •  

Let’s look at each category with examples. The programming language is practically irrelevant. The examples will be in JavaScript, but the same principle applies to other languages.

Typos

The brain is faster than our fingers. It is not surprising that automatic errors are made in the code.

A case from life. For about an hour we could not understand why the test crashes, checking the Contact value in the field. When checking the test code with spellchecker turned on, Cyrillic C was detected.

Code analyzers (Roslyn, ReSharper, SonarQube, ESLint, and others), code review and pair programming will help get rid of these errors.

Misunderstanding of logic

Often we come across code written by another developer or by us, but for a very long time, and in this case, it is necessary to understand the work of the existing logic. If we misunderstood it, the new code will probably be in error. Here are some ways to help you figure out the code:

  1. Find the author of the code. Find out the details, ask a lot of questions to make sure that you understand everything correctly. If you are an author or he is already gone, move on.

  2. Refer to the tests. Good tests describe code behavior in an accessible way.

Ask QA, the product owner, analyst, PM (or whoever you have as the custodian of knowledge about the behavior of this product).

Ignorance of the intricacies of the development language

Memory leaks, uncontrolled data modification, incorrect calculations – all these types of errors can be avoided by better studying the programming language in which you are developing.

For the input parameter value = 10, the condition will also be satisfied. We need to add a strict comparison ===.

The + operation for numbers performs addition, and if one of the parameters is a string value, then concatenation:

The count property has been changed by reference.

A case from life. When binding the context through bind, the arguments to the method come in this order: first, the parameters when binding, then the call parameters. The developer did not pay attention to this during development; as a result, the method worked only by a lucky chance with a successful order of input arguments.

Code validators and static analyzers will point out these issues.

Carelessness

The following errors can be attributed to this category:

disregarding the boundary value, for example, <instead of <= (about boundary values ​​and equivalent classes can be read here);

calling a method with a different overload;

incorrect order of operations, for example, forgot () for the expression 1 + 2 * 3;

using the wrong property/parameter.

A case from life. There was a collection dynamic cycle for the combo box with 10 elements. Later, the first element added a static value without changing the loop. As a result, the last item in the collection was not displayed.

Unit testing handles such errors well.

False tests

I believe this is the most dangerous kind of “distributor” of errors. When we are sure that the code is covered with a green test, we are unlikely to look for a bug in this area.

For example, this test will pass even if currentUser does not have the Id property populated.

Writing more “truthful” tests will help the TDD approach. We recommend reading the book of the adept TDD Roy Osherove.

Lack of error handling

If we call methods that can throw exceptions without any processing, then this is already an error on our side. You need to be able to protect your code from external influences: check input parameters (statements, boundary checks, type checks), handle exceptions, apply the Null-object pattern.

Checking the input parameters will protect the logic of the method from being used with unexpected parameter values.

Exception handling is necessary when calling external modules. This ensures guaranteed code execution and response to unpredictable situations. When processing exceptions, it is possible to log, re-execute, an alternative code branch, forward the exception further, etc.

The Null-object pattern reduces the number of type checks:

It can be implemented in this way:

Copying other people’s mistakes

Copying someone else’s code can halve development time and quadruple debugging time. There are a lot of ready-made solutions for template / simple tasks on the Internet. And often there is a temptation to “borrow” this code for your needs. However, remember: in the version control system, your name will be, not the name of the author of the original code.

A case from life. For some reason, the copied code for working with the tree did not provide for folding nested children. The error appeared only when the data for a three-level hierarchy appeared. Perhaps the author of the code did not take this into account, or maybe he did not need to provide for such a case.

Bug prevention

The best fight against bugs is to prevent them at the development stage. For this, there are many practices, methodologies, tools. Pair programming + TDD + code analyzers = a wall that confidently protects against “walking” bugs. Here are some of the approaches:

Code review

The review process involves engaging colleagues in reading and analyzing the code. This helps to identify machine, logical, structural errors, typos. It is important that one of the reviewers possess the necessary knowledge and experience. Effective for almost all kinds of errors.

Pair programming

This practice not only includes a constant real-time code review but also reduces development errors. It can also prevent staging errors, conceptual architecture errors. A bonus will be the exchange of experience, which will come in handy when correcting other errors.

Testing

Instant alarm about a bug – what could be better? It is this opportunity that tests provide us – fast, constant, effective. Testing as a tool will prevent errors due to non-compliance with logic, carelessness, lack of consideration for the intricacies of a programming language. The standard testing pyramid includes acceptance, functional, system, integration, unit tests. Mutational tests can be attributed to a separate species.

Below are unit tests that show errors in the slightest change in behavior.

TDD

Good tests are true tests. To trust your tests, we recommend that you use development through testing. With this approach, your tests will only check what is needed, and confidence in their quality will increase. In addition, the number of unnecessary lines of code is reduced. 

Code analyzers

A lot of spelling, structural errors, typos can interfere with the correct IDE configuration with code analyzers. SonarQube-type static analyzers indicate problems and describe in detail how to fix them.

Atomic implementation

Commit more often, release more often – these are habits that will potentially reduce errors. Frequent and small commits – a fast passage of pipeline testing and early feedback on the quality of the commit. This approach will help reduce the time that errors affect the product. The longer the error in the code, the more expensive it will be eliminated.

Take breaks

Perhaps not the most technical advice, but one of the most effective. Regular breaks reduce fatigue, help to focus, keep in good shape. This leads to more thoughtful development, thereby reducing the likelihood of error. You can use one of the proven techniques – Pomodoro.

Troubleshooting

Well, about what bugs are terrible and how they originate, we talked. It’s time to find and “fix” them. Ideally, a developer should have a complete list of the listed tools (and maybe more). If there are no tests, no logging or VCS, then it’s time to think about their implementation. Implementation costs will more than pay off in the future development perspective.

The methods are arranged in order of increasing importance for the relationship “time – profit”. Thus, applying one approach and moving on to the next, we increase the probability of finding an error. Of course, a developer can start at any stage, it all depends on the source data.

Bug Replication

Having received the issue from QA, the first thing you need to make sure: is there a problem? It is necessary to reproduce the error and verify the stability of its occurrence.

Check for changes.

It happens that I forgot to save some kind of config in the tab and you think why it’s not working as it should. Therefore, it is worth making sure that all source codes, configs are saved and everything is compiled.

Understand business logic

It’s impossible to fix (correctly) a bug without understanding what kind of functionality it is. The error could have arisen at the stage of setting the problem.

Check the list of common errors

A good practice is to keep a record of errors that have occurred, whether it’s a checklist or a knowledge base. A grouped list will help you identify and solve a previously identified problem in just a few minutes.

Check with tests

If they, of course, are 🙂 Tests are the best documentation of your code. It should always be clear from them how your program works. True tests will not only show the presence of an error but also suggest a way to resolve it. Again, true.

Analyze comments

We hope that logging is in your system. In debug mode, save all the information you need for analysis. For production, it is a log of the sequence of actions and errors. A well-written journal will allow you to identify the problem without rereading hundreds of lines. Event sourcing will provide even more information.

Explore Version Control System

Often, the moment a bug appears can be determined to the nearest hour. “Yesterday it worked” is one of the hackneyed phrases of the developer. And VCS will be the best custodian of change history. By looking in the log of the repository (file blame), we can confidently determine the fatal commit. In this case, meaningful comments in the commit and communication with the ticket system will greatly facilitate the understanding of the author’s intentions.

Use debugging tools

It’s time to take advantage of powerful tools for analyzing and identifying errors.

Profiling tools (dotTrace, ANTS, SQL Profiler) – to search for floating bugs, memory leaks, impact on the system.

Debuggers (ChromeTools, OzCode) – to track program execution line by line. Good command of these tools greatly simplifies the search and correction of errors.

Make a hypothesis

When the analysis fails, it’s time to turn on developer mode. Make an assumption based on your knowledge and experience where the error lies. Make changes and get fast results. Promotion should be systematic and controlled (one change at a time) so that it does not turn into a chaotic code change. 

You can take these approaches:

  • rollback method – roll back changes from VCS until the error disappears, thereby determining the desired commit;

  • version comparison – compare the behavior and result with the previous working version.

Duckling effect

If you get to this point, then it took several hours to search. You are pretty tired, lost concentration stopped focusing. Use the rubber duckling method. Tell your colleague what you have already done to detect the error. If this does not overshadow you during the discussion, then at least a colleague will help with advice.

Conclusion

We looked at ways to help reduce search time. A systematic and structured approach to finding and fixing errors significantly reduces the time spent on fixing them. Even 10% of the time gained by each developer will bring good benefits. And you will be able to devote more time to creation, but not to correction. The checklist format helps not to forget anything and establish a systematic process for detecting errors. Perhaps you are using other approaches that help you.

 

Leave a Reply