Kinnu

Debugging

Understanding Debugging

What is debugging?

Debugging is the process of finding and fixing errors, or 'bugs', in computer programs. The reality of backend development is that not everything will go as planned.

Debugging. Image: Kai Hendry, CC BY 2.0 <https://creativecommons.org/licenses/by/2.0>, via Wikimedia Commons

There's a popular story that 'debugging' is named after literal bugs. According to this story, a Harvard software developer, Admiral Grace Hopper, coined the term when one of her colleagues found a moth trapped in one of the university computers, preventing the system from working. The moth had to be removed, or 'debugged', before the system started working again.

This story is probably an urban myth, but it's a good way to remember the term.

Common bugs

Common issues that come up during debugging include syntax errors, logical errors, and runtime errors.

Syntax errors are mistakes in the code's syntax, like a missing parenthesis or a misspelled keyword. Logical errors are mistakes in the program's logic, like when a developer writes a function that should take an average of some numbers, but wrote the formula incorrectly, and calculated a sum instead.

Syntax errors. Image: Tyrank411, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

Runtime errors occur while the program is running. They can be caused by a variety of issues, like if the programmer put the code in a position where a number would be divided by zero. These errors often result in the program crashing or behaving unpredictably.

Fixing bugs

Developers should take a systematic approach to debugging: form a hypothesis about the cause of the error, test this hypothesis, interpret the results, then come up with a solution.

Hypothesis testing. Image: Brightyellowjeans, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

As part of the hypothesis testing stage, it is helpful to reproduce the error. This involves recreating the conditions under which the error occurred in order to better understand its cause.

Keeping a debugging log is also helpful. This log records the steps taken during the debugging process, allowing developers to track their progress and avoid repeating the same steps twice. It serves as a valuable reference that can help streamline the debugging process.

Testing and Debugging Techniques

Unit testing

Debugging an entire program at once can be a daunting task, which is why Backend Developers use unit testing.

Unit testing verifies the individual parts, or units, of a computer program, instead of seeing whether the entire program works all at once. This approach ensures each part of the program works correctly before they are integrated together.

Imagine baking a cake. Unit testing would involve individually inspecting each ingredient in the cake – smelling the eggs, sifting the flour, proofing the yeast – before putting it into the mix. That way, it’s easier to pinpoint any bad ingredients in advance. This approach is much easier than baking the cake, discovering it tastes bad, then trying to work out which ingredient was wrong in hindsight.

Test cases

During unit testing, Backend Developers write test cases for each unit. These test cases are designed to check if the unit behaves as expected under different conditions.

For example, for a unit of code that calculates the square of a number, a developer might test if the code returns the correct square for a positive number, for a negative number, for zero, and so on.

Automated tools can simplify the process of unit tests. Tools like JUnit and NUnit can execute a suite of different tests and provide a detailed report of the results.

Integration testing

While unit testing is important, Backend Developers also need to make sure that these units still work when combined together as a whole. That’s why developers do integration testing too.

Integration testing is when individual units are combined and tested as a group. For example, you might have a backend application that includes different modules responsible for user authentication, data processing, and API interactions. Integration testing ensures that these modules work harmoniously when combined.

Unit testing, integration testing, system testing. Image: Jouasse, CC BY-SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0>, via Wikimedia Commons

To use another cake analogy: unit testing can check that your cocoa powder tastes good, and your lemon zest tastes good, but integration testing makes sure that the chocolate and the lemon taste good when you put them together.

Different approaches

If a developer starts with unit testing, before moving onto integration testing, it is sometimes described as a bottom-up approach – focusing on the small parts, then worrying about the larger system later.

Another developer might take a top-down approach – they will start by testing the integration of the overall system, before focusing on perfecting each individual unit.

Choosing between these two approaches depends on the nature of the project and the developer's personal preference. The bottom-up is good at pinpointing specific code issues, but may miss larger integration issues. Top-down debugging will catch larger integration issues, but might overlook specific lower-level bugs.

Advanced Debugging Strategies

Stubs and drivers

Developers taking a top-down approach will run into a problem: how do you test the integration of units when some of those units don't exist yet? The solution is something called stubs and drivers: placeholder units that developers use to simulate the behavior of units that haven't been built yet.

A stub is a simplified version of a unit, which basically works as a test dummy. Another unit can interact with it, and those interactions can be tested and debugged, but the unit itself isn't capable of doing much else.

A driver, on the other hand, is used to send inputs to other units, so that developers can check whether those units are handling these inputs correctly. For example, a developer may have built a database to store usernames, but they haven't yet built a unit for user registration. They can still test if the database is working correctly by using a driver to generate some usernames, then observe how the database handles them.

Debugging tools

Debugging tools are invaluable aids for developers in identifying and fixing errors in their code. These tools provide a range of features that simplify the debugging process and make it more efficient.

Debugging tools offer features like breakpoints and call stack visibility. Breakpoints are points in the code where the debugger will pause the execution, allowing a developer to inspect the program's state at that specific moment. Call stack visibility is a feature that displays the sequence of calls that led to the current point in the code. This hierarchy is essential for understanding the flow of a program, and identifying the path that led to a particular issue.

There are many debugging tools available, each with its unique features and capabilities. Some of the popular ones include Visual Studio Debugger, Chrome Dev Tools, and Firebug.

Overcoming frustration

Debugging can be frustrating, but it's an essential part of a Backend Developers role.

A failure to do proper debugging can lead to the accumulation of something called 'technical debt'. This is the idea of taking shortcuts or making compromises during the development process, which ultimately results in suboptimal code quality, and a lot more work when issues pop up down the line. Debugging helps to keep that future work – or technical debt – as low as possible.

Debugging can also feel less daunting when developers engage in collaborative code reviews, also known as pull requests. These are processes by which a group of developers examine, analyze, and provide feedback on each other's code.