CS 235/250 Unit 03 Exercise: Debugging and testing

Table of Contents


1. Introduction

About
For this assignment we’re going to be exploring how to use the debugging tools of Visual Studio, Code::Blocks, and/or Xcode, as well as navigating the IDE with commands such as “go to definition”.
Goals
  • Debugging:
    • Be able to use breakpoints to step through code.
    • Use the local and/or watch window to view variable values.
    • Use the call stack to view function call history.
    • Utilize features of the IDE to navigate code better.
  • Testing:
    • Learn how to write test cases to validate your program functionality.
    • Learn how to write unit tests.
Setup
Make sure to download the following starter files:
  • Zombie Game (debugging)

2. Working with debugging tools

Make sure you've downloaded the "Zombie Game" starter code. It will contain the following source files:

  • game.cpp
  • ZombieGame.cpp
  • ZombieGame.h

Link to Zombie Game files: https://gitlab.com/rachels-courses/cs235-object-oriented-programming-with-cpp/-/tree/2023-08_FALL/U03EX/Debug_ZombieGame?ref_type=heads

Link to Testing files: https://gitlab.com/rachels-courses/cs235-object-oriented-programming-with-cpp/-/tree/2023-08_FALL/U03EX/Tests?ref_type=heads

Create a new project in whichever IDE you prefer and add these files to your project.

2.1. Running the game as-is - Learning the controls

----------------------------------------
-- Moose                              --
-- Day 1   Food: 5   Health: 100/100    --
-- Location: Overland Park            --
----------------------------------------
-- 1. Scavenge here                   --
-- 2. Travel elseware                 --
----------------------------------------

Run the game as-is and play a few rounds of it. You can choose to scavenge in the area you're currently at, or travel to another location. For this step, you're just getting used to how the game controls.

2.2. Using breakpoints - Pause program execution at some line

Task: Use breakpoints and the locals/watch windows to take note of game variables:

  • m_day
  • m_food
  • m_maxHealth
  • m_health
  • m_location
  • m_done

Observe the current values of those variables. Note them down on a piece of paper. Refer to the following documentation on the steps to do this.

2.2.1. Setting the breakpoint:

Your first breakpoint will go at the inside the ZombieGame::Setup() function within ZombieGame.cpp, at the line m_day = 1;

void ZombieGame::Setup()
{
    srand( time( NULL ) );

    cout << "Z O M B I E  -  A P O C A L Y P S E" << endl;
    cout << "Enter your name: ";
    getline( cin, m_name );

    m_day = 1;
    m_food = 5;
    m_maxHealth = 100;
    m_health = m_maxHealth;
    m_location = "Overland Park";
    m_done = false;
}

To set a breakpoint, you can locate a line of code and click on a region in your editor next to the line number that will enable a breakpoint. A breakpoint is a debugging tool that will cause the program execution to pause when that line of code is reached. While the program is paused, you can investigate values and step through the program flow one line at a time.

c3_u03_00setbreakpoint.png

Depending on your IDE you might need to click before or after the line number to set the breakpoint. You can also toggle breakpoints through the menu or keyboard shortcuts:

IDE Menu Shortcut
Visual Studio Debug, Toggle breakpoint F9
Code::Blocks Debug, Toggle breakpoint F5

Next you'll need to run your program in debug mode:

IDE Toolbar Menu Shortcut
Visual Studio ▶️ Local Windows Debugger button Debug, Start debugging F5
Code::Blocks ▶️ (Red play button) Debug, Start/continue F8

The program will build (if it hasn't already) and start running the program. When first running the program it will ask for the player's name. After the name is entered, the program will hit the breakpoint and pause execution. Go back to your IDE to view where your program has paused.

2.2.2. Opening the variable investigation panes:

At the bottom of your screen should be some debug panels. If they don't show up, you can usually open these under the Debug drop down menu.

IDE Steps
Visual Studio Debug, Windows, Watch window
Code::Blocks Debug, Debugging wnidows, Watches
XCode View, Debug area, Activate console

Now you should have the watch windows open, which is a table that displays a Name (variable name) column and a Value column.

c3_u03_01locals.png

2.2.3. Enter the names of the variables we want to track:

In these watch windows, type in the names of the following variables:

  • m_day
  • m_food
  • m_maxHealth
  • m_health
  • m_location
  • m_done

2.3. Step over - Viewing variables as they change values

Task: Use breakpoints and the locals/watch windows to take note of game variables:

  • m_day
  • m_food
  • m_maxHealth
  • m_health
  • m_location
  • m_done

Step through your program inside the ZombieGame::Setup() function within ZombieGame.cpp, stopping at the line m_done = false;

Observe the current values of those variables. Note them down on a piece of paper.

While the program is still paused, we will use the step features to step through each line of code one at a time and watch the variables as they're assigned values.

Use the Step Over or Next Line to step down one line at a time until you get to the end of the function. As you go down each line, notice how the variable value changes in the watch window once its declaration line is finished.

c3_u03_02stepover.png

2.4. More with breakpoints - Investigating program flow

Task: Set a breakpoint at the start of ZombieGame::Menu_Scavenge() and then use the Continue button to resume program execution. Select the "1. Scavenge here" option in the game and make sure the program execution pauses within this function.

Afterwards, use the Step Over / Next Line button to see which if/else if statement is executed: randomChance = 0=? randomChance = 1=? And so on…

Take note of what values of randomChance and mod.

Play two turns, logging the flow in ZombieGame::Menu_Scavenge() and the values of randomChance and mod each time.

2.4.1. Navigating to the next breakpoint

Now we're going to set another breakpoint and watch how the program "flows" after we've made a decision and have code that has branching.

Set a new breakpoint at within the ZombieGame::Menu_Scavenge() function, right at the if statements that are checking the value of the randomChance variable:

void ZombieGame::Menu_Scavenge()
{
    cout << "* You scavenge here." << endl;

    int randomChance = rand() % 5;
    int mod;
    if ( randomChance == 0 )

If your program is still running, you can use the continue button to continue running from where the pause cursor is currently at. The program will continue running until it hits another breakpoint.

IDE Toolbar Menu Shortcut
Visual Studio ▶️ Continue Debug, Continue F5
Code::Blocks ▶️ (Red play button) Debug, Start/continue F8

Continue playing the game, choosing "1. Scavenge here" once you're on the main game screen. After selecting this and hitting enter, the next breakpoint will trigger and we can return to the IDE.

2.4.2. Following the program flow

Use the Step Over/Next Line action to move forward in the code. A random value will be given to the randomChange variable, and then there is a series of if/else if statements.

Once the program flow exits the if/else if statement block it will be at the end of the ZombieGame::Menu_Scavenge() function. What values of randomChance and mod were assigned? What is the value of the variables we logged previously?

Play two turns, logging the flow in ZombieGame::Menu_Scavenge() and the values of randomChance and mod each time.

2.5. Call Stack - Looking at the function call history

Another important debugging tool is the Call Stack, which tells you the order of functions called to get to where the program is currently at.

Task: Use the Call Stack to take note of what functions were called leading up to your ZombieGame::Setup() breakpoint.

Set a breakpoint in ZombieGame::Setup() again, and run the program until you get to the breakpoint. The call stack should look like this:

c3_u03_03callstack.png

If the call stack pane isn't showing up, you can open them through the menus:

IDE Menu
Visual Studio Debug, Windows, Call Stack
Code::Blocks Debug, Debugging windows, Call Stack

The top-most function displayed is the one currently running, and all functions below it are functions that were active previously. If you step all the way down, you can trace all the functions that were called to get to where we're at.

Task: Set a breakpoint in the ZombieGame::Menu_EndOfDay() function and use the Call Stack to take note of what functions were called leading up to it.

Task: Set a breakpoint in the ZombieGame::DisplayStats() function and use the Call Stack to take note of what functions were called leading up to it.

2.6. When to use debugging tools…?

Debugging tools are extremely helpful and the better you get at utilizing them to diagnose your programs' issues, the more well prepared you will be for software development in the "real world".


3. Navigating your IDE

Don’t close the zombie game yet – now we’re going to use various IDE features in this project.

3.1. Viewing code split-screen

In most IDEs you can click-and-drag a code window (via its tab) to another part of the screen, allowing to have a side-by-side view of two (or more) different code files at once.

Visual Studio:

Click and drag the tab to the center of the screen. Then, a placement box will show up. Drag the tab to one of these 5 options for different views.

c3_u03_04draggingA.png

Code::Blocks:

Click and drag the tab to an edge of the code window. A slight highlight will appear in the place where it will be placed. Let go of the mouse button to place the tab to the side.

c3_u03_04draggingB.png

3.2. Go to definition

Go to the .h file and right-click on one of the functions, then select the Go to definition (VS) or Go to implementation (CB) to go directly to the definition of that function.

c3_u03_05definition.png

3.3. Go to declaration

Right-click on a function call somewhere, and select Go to declaration to go to where the function is declared.

c3_u03_06declarations.png

3.4. Find references of

Right-click on any function and select Find references of and you will get a search result with every place where the function is used throughout the program, including function calls.

c3_u03_07references.png

3.5. Indenting/unindenting chunks of code

Highlight a chunk of code and use "tab" to indent it all forward, or "shift+tab" to un-indent it all backward.

c3_u03_08tab.png

3.6. Autoformatting

When your indentation gets messed up you can have the IDE automatically format it for you.

c3_u03_09format.png

IDE Steps
Visual Studio Edit, Advanced, Format Document
Code::Blocks Right-click in the code window, select Format use AStyle

3.7. Comment/uncomment code blocks

Highlight a section of code. You can comment out a whole block at once, or uncomment out a block, in the following ways:

c3_u03_10commentout.png

IDE Comment-out Uncomment-out
Visual Studio Hold CTRL, then press K and then C Hold CTRL, then press K and then U
Code::Blocks Press CTRL+SHIFT+C Press CTRL+SHIFT+X

4. Testing

Make sure you've downloaded the "Test" starter code. It will contain the following source files:

  • main.cpp
  • Functions.h
  • Functions.cpp

This program contains four simple functions and four test functions:

  1. int GetArea( int width, int length );
  2. void Test_GetArea();
  3. int GetStringLength( string text );
  4. void Test_GetStringLength();
  5. int GetPerimeter( int width, int length );
  6. void Test_GetPerimeter();
  7. float Withdraw( float balance, float amount );
  8. void Test_Withdraw();

We are going to approach this from a Test Driven Development standpoint where we will write our TESTS before implementing the functionality. This way, we ensure our tests "work" because they first fail - then, when we implement the functionaltiy, the tests passing should be how we verify our logic is correct.

4.1. Set 1: Stub functions

These functions contain placeholder information being returned. They return data because some C++ compilers will complain if a non-void function doesn't have a return statement. Before you implement the function itself, we will be implementing the unit tests for each function first.

Task: Implement void Test_GetArea() and void Test_GetStringLength() first, then implement int GetArea( int width, int length ) and int GetStringLength( string text );

Read the following instructions for a guide.

4.1.1. GetArea

First off we need to come up with test cases - at least two for these simple functions that just do computations. Why two? Because if you only have one test that checks "The area of a 2x5 yard is 10", then implementing the function as return 10 would cause it to pass… we need at least two tests to make sure the function has proper logic.

Here are some test cases we can use for GetArea:

# Inputs Expected outputs
1 width=5, height=3 returned area is 15
2 width=7, height=11 returned area is 77

So… how do we implement this in code?

We have two inputs and an expected output, and in the program itself we will have an actual output. We can create variables to represent each of these pieces of information:

  • int input_width = 5;
  • int input_height = 3;
  • int expected_result = 15;
  • int actual_result;

To get a value for the actual_result, we just call the function itself, passing in our inputs:

actual_result = GetArea( input_width, input_height );

We can tell if the test passed if the actual_result matches the expected_result. Or, the test failed if they do not match.

It would be useful to output the results to the console as well, so we can write out something like this:

if ( actualResult != expectedResult ) { result = "[FAIL]\t"; }
else                                  { result = "[PASS]\t"; }

cout << result << testName << endl;
cout << "* input_width:     " << input_width << endl;
cout << "* input_height:    " << input_height << endl;
cout << "* expectedResult:  " << expectedResult << endl;
cout << "* actualResult:    " << actualResult << endl;
cout << endl;

If you run the program now, we should get a nice print out of the test result:

[FAIL]  GetArea - pass in 5 and 3 and should get 15 back
* input_width:     5
* input_height:    3
* expectedResult:  15
* actualResult:    -1

Implement the second test case in the code before continuing.

Once they're both implemented, you can now implement the int GetArea( int width, int height ) function. To calculate the area of a rectangle, we use the formula:

area = width x height

(But write it in C++ code :)

Implement the function and once run the tests should now pass. Here's how I've formatted my tests:

--------------------------------------------------------------------------------
Test_GetArea
--------------------------------------------------------------------------------
[PASS]  GetArea - pass in 5 and 3 and should get 15 back
* input_width:     5
* input_height:    3
* expectedResult:  15
* actualResult:    15

[PASS]  GetArea - pass in 7 and 11 and should get 77 back
* input_width:     7
* input_height:    11
* expectedResult:  77
* actualResult:    77

4.1.2. GetStringLength

Implement the following test cases as unit tests:

# Inputs Expected outputs
1 text="Hello" returned length is 5
2 text="abc def" returned length is 7

Once the tests are implemented and they fail when you run the program, now implement the GetStringLength function. (Hint: You can get the length of the text string with text.size().)

4.2. Set 2: Broken functions

These functions have already been implemented, but with incorrect logic. You're going to write unit tests before doing any fixes to help ensure that your fixes are valid once implemented.

Task: Implement void Test_GetPerimeter() and void Test_Withdraw() first, then fix the functions -int GetPerimeter( int width, int length ) and float Withdraw( float balance, float amount )

Read the following instructions for a guide.

4.2.1. GetPerimeter

For this function, come up with your own TWO test cases and implement them as unit tests. After implementing the unit tests, fix the function logic.

4.2.2. Withdraw

For this function, come up with your own TWO test cases and implement them as unit tests. After implementing the unit tests, fix the function logic.


Author: Rachel Wil Sha Singh

Created: 2023-09-07 Thu 10:35

Validate