You are on page 1of 1

Ryans Tutorials

Software Design and Development - Debugging Techniques Tutorial


More Sections
Tutorials

SECTION BREAKDOWN

Introduction

Types of errors

Preventing errors

Debugging Techniques! Testing for errors

Debugging Techniques

Squish those bugs. Mindset

The big picture

Summary

Introduction
It is inevitable that when you are writing code you will end up with bugs. If poorly managed, the time you spend debugging your code
could be greater than the time spent actually writing it. There are various things you can do to help minimise this however.

Various development environments contain tools to help identify bugs. I will not discuss those here but instead look at general
techniques which may be applied no matter the language you are developing your software in.

The code examples on this page are in a fictional language called RyanScript. I have done this deliberately as there are many
programming languages out there, each different in their syntax. The concepts discussed on this page are generic and apply no matter
which language you are developing your software in.

Three types of errors


There are three types of errors you will encounter.

Syntax errors
Logic errors
Runtime errors

Syntax errors are when you have written part of your code with a mistake. It could be a spelling mistake or a missing character for
instance. A very common syntax error is to forget the semicolon at the end of a statement. Another common mistake is to misspell a
variable name.

These errors are often quite easy to fix as the compiler or interpreter will halt with a message telling you which line the syntax error
occurred on and what it was expecting.

Logic errors involve your code being syntactically correct but it does not behave how you would expect it to. Maybe a certain action is to
occur when a variable gets above a certain value however when you run the program that action does not occur unless the variable is
exactly that value.

These errors can be difficult. Identifying the section of code responsible can be tricky and you won't get an error message. It may also be
the case that the software functions properly most of the time and only works incorrectly under certain circumstances.

Runtime errors occur when a situation occurs during the running of your code which results in the software not being able to continue
execution. Exampes of situations which may cause this are a divide by zero or the software trying to open a file which does not exist.

These errors are sometimes easy to identify and sometimes difficult. If it is an error regarding a missing or incorrectly named file for
instance identifying the cause of the problem and the fix should be easy. If it is something like a divide by zero then working out why it
occurred may be easy but working out how to prevent it happening may be trickier. These errors can often be caused by logic errors and
similarly often occur only sometimes and under certain circumstances.

Prevention is better than the cure


We will look at various techniques to identify and squash bugs below. Before we do so, let's look at some things we can do to minimise
the number of bugs we produce in the first place.

Taking time to design your software in a neat and logical structure before you even begin coding will lead to your code being cleaner
and less prone to errors.

Once you start coding, if you follow Good programming practice you will also write code which is less prone to errors and easier to debug
when those errors do occur.

Utilising libraries and built in functions whereever possible can also help as you will be making use of code which is already tested and
solid.

Note

Although generally a good idea to make use of libraries where possible there is a line of thinking that in certain circumstances
where performance is important, writing your own code can be leaner and more efficient. This is particularly the case when
writing client side web based applications.

Testing
There are two general categories of testing. Black box testing and white box testing.

Black box testing involves testing the code, or part of the code (Eg. a function) without actually looking at the code itself. We run the
product giving a variety of different inputs and ensure that it gives the required outputs and behaves the way it should. If something goes
wrong we generally don't make any assumptions about why, we just note that it did. Because of the nature of black box testing it can
easily be done by an end user. This can be effective as they are more likely to use it in ways that the developer never thought of.

Black box testing gets its name as the internals or the actual processing is dark to the person doing the testing. They can't see it.

White box testing involves testing of the code whilst being able to see the processing and interact with it to better understand what is
going on. This is usually done by a developer and often after black box testing has revealed bugs which need to be investigated. The
developer has a variety of tools at their disposal to help them including CASE tools and the debugging techniques discussed below.

Note

Testing can show you that there are bugs. It cannot prove there are no bugs. Good testing will increase confidence in the
quality of your code but never assume there are no bugs (except for small, trivial programs).

Error Messages
There is an acronym in IT which is RTFM, Read the _______ Manual. (I'll let you figure out what the missing word is.) (I don't expect to
hear you say this word either) I would like to propose a similar acronym RTFEM. The last two words are error messages. I don't mean
just skim the error I mean really read and consider what it is saying. This might seem obvious but I get an unusually large number of
requests from students for help with their code and when I ask them if they have read the error message they say no.

Debugging Techniques
Debugging output statements
One of the easiest and most effective ways to start debugging your code is to start printing things out. Printing a message within
branches of an IF statement will help you determine which branch is actually being executed. Let's say we have a simple program which
asks you for a score, adds 10 to it and if the score is above 20, saves to the database.

wrong_branch.rs

1. myscore = input('Please enter your score: ');


2. myScore = myScore + 10;
3. if (myScore > 20) {
4. save_to_high_scores(myScore);
5. }
6. playAgain = input('Play again? ');

When you run this program however, no matter what the score entered is it never saves to the database. You could decide that the
problem must be with the database and go looking for a bug within the function save_to_high_scores. It would be smarter to test that
the function is actually being called first though. Let's put a debugging output statement in and make sure it works:

wrong_branch.rs

1. myscore = input('Please enter your score: ')


2. myScore = myScore + 10
3. if (myScore > 20) {
4. print ('Saving to database');
5. save_to_high_scores(myScore);
6. }
7. playAgain = input('Play again? ');

Now we run the program again and notice that the if statement never seems to be entered. This tells us that the problem might not be
within the function but is happening somewhere within the current code. Now we might decide to try another strategy which is to print out
variables to track how they are changing throughout execution of the code.

wrong_branch.rs

1. myscore = input('Please enter your score: ');


2. myScore = myScore + 10;
3. print ('myScore is: ' + myScore);
4. if (myScore > 20) {
5. save_to_high_scores(myScore);
6. }
7. playAgain = input('Play again? ');

We run this and notice that the value of myScore is 10 no matter what we enter. This is perplexing. When this happens, a good strategy
is to progressively move the print statement up through the code or to add multiple print statements so you can see a history of how the
variables value changes.

wrong_branch.rs

1. myscore = input('Please enter your score: ');


2. print ('myScore is: ' + myScore);
3. myScore = myScore + 10;
4. print ('myScore is: ' + myScore);
5. if (myScore > 20) {
6. save_to_high_scores(myScore);
7. }
8. playAgain = input('Play again? ');

Now we realise that myScore is zero even before running the addition so the problem is not on line 3. It must be on the first line, and on
closer inspection we realise that we have made a typo and written myscore instead of mySscore. Problem solved and the potential to
waste a lot of time looking in the wrong location for the but has been averted.

Flags
Flags are useful in conjunction with debugging output statements. A flag is a variable that we set to indicate that a certain section of code
or event has occurred. eg. maybe we wish to know if a particular branch of an if statement has been entered or if a particular function
has been called. Alternatively we may wish to know how many times a loop has run. We will create a variable, which may be a boolean if
we just want to see if some code has been run, or an integer if we want to know how many times a loop has run. Then we will use a
debugging output statement to print the variable to help us understand what has happened.

high_score.rs

1. score = read_score_from_db ( userID );


2. level = highest_level_reached ( userID );
3. bonusFlag = False; # this is the flag
4.
5. if ( score > 500 and level < 10 ) {
6. bonusFlag = True;
7. score = score + 100;
8. }
9.
10. print ( bonusFlag );

Commenting out sections of code


An important stratety in debugging is divide and conquer. When searching for a bug we want to progressively identify sections of code
we are certain the bug is not in to help us narrow down our search. Sometimes there is processing which we can remove and still have
the rest of the code function. Consider the following code which reads user details from a database, orders them in a particular way, then
prints them to the screen:

users_print.rs

1. users = get_users_from_db ();


2.
3. users = sort_by_employment_level (users);
4.
5. print_users (users);

When we run this code only the first two users are printed to the screen. The problem could be in any of the three functions called above.
We make an assumption that the second function could be the culprit so we comment it out. If the users are all printed (though not in the
right order) then we can assume that the bug is probably in the function sort_by_employment_level and start looking there.

users_print.rs

1. users = get_users_from_db ();


2.
3. # users = sort_by_employment_level (users);
4.
5. print_users (users);

This debugging technique is only useable when we can comment out code and still have the rest of the code function properly. If it is not
possible to do this then a driver or stub might be what you need.

Drivers
A driver is a small piece of code which allows you to run another piece of code in a managed way. This can be useful for testing a
function we have written. Lets say we have a function which accepts an array of integers as its parameter and sorts them in desending
order. We have just written it and wish to test it as it is essential in the running of other code we wish to write next. We want to run it in a
managed way which will allow us to re run the tests several times easily as we iron out any bugs we find. Here is an example of what a
driver might look like in this scenario:

test_sorting.rs

1. function sorting_driver () {
2. print ("Test 1: 5, 4, 7, 1, 8");
3. print ( sort_array([5, 4, 7, 1, 8]);
4. print ("Test 1: 3, 8, 2, 9, 8");
5. print ( sort_array([3, 8, 2, 9, 8]);
6. print ("Test 1: 1, 2, 3, 4, 5");
7. print ( sort_array([1, 2, 3, 4, 5]);
8. }
9.
10. function sort_array (numbers) {
11. #code here that is probably buggy
12. }
13.
14. # main();
15. sorting_driver();

Tip

Picking the right tests to run is important in the effectiveness of your driver. You want to make sure you think of possible
scenarios in which the code might break but also in which it does work to help you narrow down what the problem might be.

Stubs
A stub is the opposite of a driver. Whereas a driver sits above the code you have written and allows you to run it (ie. drive it), a stub takes
the place of code which has not been written yet. Stubs allow you to test code which relies on functionality which you have not yet
implemented. Lets say you have written some code which prints the top 10 scores for a game you are developing. The scores will be
stored in a database but you haven't written the functions for reading from the database yet. Instead of calling the function which would
read from the database to retrieve the data, we can instead call a function which will just return some static data for the purposes of
testing if we can render the data properly.

test_top_ten.rs

1. function render_top_ten () {
2. # var topTen = get_scores_from_database();
3. var topTen = get_scores_from_stub();
4.
5. # code to render the top ten scores
6. }
7.
8. function get_scores_from_database () {
9. # code not written yet
10. }
11.
12. function get_scores_from_stub () {
13. return [[5, 'Hitman'], [23, 'Denogginator'], [45, 'WiseGuy'], ...];
14. }

Desk checking
Using the above techniques, we think we have narrowed down where the bug is in our code. Now we need to work out just what is
happening.

Desk checking involves manually working through a piece of code and tracking the values of variables as we go. It is a particularly
effective debugging technique when we have a piece of code that is manipulating data.

We perform our desk check as a table with the column headings being the names of the variables. If we had the following code which
finds the highest even number in an array of numbers:

dummy_processing.rs

1. function get_largest_number (numbers) {


2. var largest = 0;
3.
4. for ( i in numbers ) {
5. if ( i % 2 == 0 ) {
6. if ( i > largest ) {
7. largest = i;
8. }
9. }
10. }
11.
12. return largest;
13. }

We would step through the code ourselves line by line and produce the following table:

numbers largest i i%2 return

[4, 5, 2] 0 4 0

5 1

2 0

Note

Notice that the values within the columns have been entered with gaps where appropriate to show the chronological order of
operations. This makes it easier to trace back through the processing if required.

Tip

Sometimes it's useful to include columns which aren't variables but are specific bits of processing. The fourth column above
isn't a variable but is processing that we want to ensure works properly so we have listed it as something to track. There aren't
any specific rules about what we do and don't enter for our columns but generally the more detail the better. All variables
should be listed as a minimum though.

Picking the right test data is important in maximising the value of desk checking. Sometime the processing will work under some
conditions but not others. In general you want to make sure you check boundary values and differing scenarios. eg, for the following
code we may decide to desk check with the following sets of test data:

[2, 4, 6] - all even numbers, last number is largest.


[8, 2, 4] - all even numbers, first number is largest.
[2, 8, 4] - all even numbers, middle number is largest.
[4, 8, 6, 8] - what if there are multiple of the same number?
[3, 7, 4, 11] - only a single even number.
and so on.

When desk checking multiple sets of inputs you can check them in separate tables or to save yourself writing out the titles every time you
can just list them on one table directly under each other. Separate the different tests with a horizontal line.

When desk checking it is good to know what the expected output / result should be for a given input. Sometimes you can tell just by
looking at the data what the result should be, sometimes you may need to work it out yourself. If you have created IPO tables for the
processing as part of understanding the problem then referring to those can help you define the expected results.

Move code into a smaller program


Another technique to use once you think you know where the bug might be is to play about with a simpler piece of code in another file.

Sometimes it is useful to verify different sections of code to help you narrow down where the problem is. If various sections of code may
be run independently then copying that code to a separate file and testing it by itself can make it easier. You may need to add in some
stubs, drivers and debugging output statements but it allows you to be certain that errors aren't being caused by something somewhere
else in the code.

For instance, maybe we are making a game and the character is not responding to our button presses. Is the error in recording events
(keyboard presses), processing the actions or rendering the new data? We can start by copying the code for recording events into a new
file and getting it to set a flag according to the inputs then print that flag. If this works we may move on to look at the processing section
of the code. If it doesn't work we can easily play about with it and experiment without other bits of code getting in the way.

Peer checking
It is common for people to overlook, and keep overlooking their own mistakes. It is often the case that the error is simple and obvious but
because you have a particular train of thought (the same train of though that lead to the error in the first place) you just keep overlooking
it. Getting someone else to have a look over it can be very powerful. It can be even more effective if that person is not invested in the
product and has not seen the code before. This is called the Uninterested observer principle and can be very effective. Don't overlook
this valuable technique or think you shouldn't use it out of fear that others may doubt your skills.

Give it some time and come back


This is another simple yet very effective debugging aid. It is common to spend ages looking at a problem and get nowhere. Leave it for a
period of time and come back to it and identify the error near instantly. It is also common that the reason for the bug comes to mind whilst
doing something completely unrelated. Don't underestimate the power of this and if you are nowhere with a bug it is often much better to
do this than persist with banging your head against a brick wall. Often the more you persist with a bug the more stressed you get and the
more stressed you get the less flexible and creative your mind becomes which is the exact opposite of what is required for good bug
squashing.

The right mindset


You will be more effective whilst utilising the debugging techniques discussed above if you are in the right mindset. A calm, methodical
and open mindset whilst debugging will make the experience more effective.

The big picture


It's a rare person that enjoys debugging. Still it is something which needs to be done to ensure the quality of your code. It would be a
shame to have gone to all the time and effort of understanding the problem, create a solid plan and build the solution only to have the
result not work very well due to bugs. Even small, inconsequential bugs, will affect the confidence users have in your product so it is vital
that this stage is done well.

Summary
Stuff We Learnt

Debugging Output Statements


Print the values of variables to verify your assumptions about the processing.

Commenting out sections of code


Reduce the processing to narrow down where bugs might be.

Drivers
A small piece of code which allows you to run and test another section of code under controlled conditions.

Stubs
A placeholder for another section of code. Usually returns hard coded values.

Flags
A variable that is set to indicate particular code has been run or scenario has occured.

Desk Checking
Manually working through your code by hand.

Move code to a smaller program


Work on a section of code in isolation to make things simpler.

Peer Checking
A fresh set of eyes will often spot things which you miss.

Important Concepts

Think about your data


The right test data will make it easier to spot and undertand bugs in your code.

Look for the details


Don't just see that there is something wrong. Look for the details which will help you understand why it is wrong.

Calm and Methodical


Debugging is more effective with the right mindset.

 Good Programming Practice

Follow @funcreativity
By Ryan Chadwick © 2019

Home Linux Tutorial HTML Tutorial Binary Tutorial

Education is the kindling of a flame,


not the filling of a vessel.
- Socrates

Bash Scripting Tutorial CSS Tutorial Regular Expressions


Contact | Disclaimer

Programming Challenges Problem Solving Boolean Algebra Tutorial

Basic Design Tutorial Solve the Cube Software Design and


Development

micro:bit Tutorial

You might also like