Appendices

Testing and Debugging

Apply testing and debugging techniques to C programs

"The most effective debugging tool is still careful thought, coupled with judiciously placed print statements" (Kernighan, 1978)

Errors | Testing | Exercises



Testing and debugging skills are integral skills that a software developer refines throughout her career.  A program that executes successfully for a well-defined range of values might still crash for values outside this range.  Each program should be throughly testing before release to the user community.  Compiler identify the syntactic errors with respect to the rules of the language, but cannot readily identify semantic errors; that is, errors in the meaning or intent of the code.  Walkthroughs and code analysis help identify semantic errors. 

This chapter lists the common types of errors encountered and some techniques used to identify them.  This chapter introduces the gdb runtime debugger and two approaches to testing software. 


Errors

Syntactic Errors

The most common syntactic errors in the C language are:

  • missing semi-colon
  • unnecessary semi-colon terminator in a #define
  • undeclared variable name
  • mis-matched parentheses
  • left-side of assignment is not a defined memory location
  • return statement missing

Techniques for identifying syntactic errors include

  • reading code statements (walkthroughs)
  • compiler error messages (compiler output)
  • comparing error messages from different compilers - some are more cryptic than others

Semantic Errors

The more common semantic errors are:

  • = instead of ==
  • iteration without a body (for/while followed by a semi-colon)
  • uninitialized variable
  • infinite iteration
  • incorrect operator order in a compound expression
  • dangling else
  • off-by-one iteration
  • integer division and truncation
  • mis-matched data types
  • & instead of &&

Techniques for identifying semantic errors include:

  • vocalization - use your sense of hearing to identify the error (compound conditions)
  • intermediate output - printf() statements at critical stages
  • walkthrough table
  • interactive debugging using
    • Visual Studio IDE - integrated debugger for Windows OSs
    • Eclipse IDE - integrated debugger for Linux OSs
    • gdb - GNU debugger for gcc

gdb

gdb ships with the gcc compiler.

Compile and Run

To be able to use gdb, compile your source code with the -g option

 gcc -g myProgram.c

To debug the executable (a.out), enter

 gdb a.out

The gdb prompt will appear

 (gdb) 

Commands

gdb commands to enter at the prompt include:

  • list - lists the 10 lines of source code in the vicinity of where execution has stopped.  Each call advances the current line by 10
  • list m, n - where m and n are line numbers - lists lines m through n inclusive of the source code.  This call advances the current line to n+1
  • break n - where n is a line number - sets a break point at line number n
  • clear n - where n is a line number - clears any break point or trace at line number n
  • delete - clears all breakpoints
  • run - starts the execution of your program from line 1
  • print varname - where varname is a variable name - displays the value of varname
  • cont - continues execution until either your program ends or encounters a breakpoint
  • step - executes one line of your program
  • help - displays the full set of commands available
  • quit - quits

gdb is case sensitive.

When you start gdb, your program pauses.  This is your first opportunity to set breakpoints.  Enter the run command only once you are ready to execute.

Crashes

If your program crashes and produces a core dump, gdb helps locate the crash point.  Enter

 gdb a.out core 

and use the following commands

  • where - displays the function and line number at the time of the crash
  • up - moves up one function in the stack (towards main())
  • down - moves down one function in the stack (away from main())

Help

For online help with a particular command while debugging, enter

 help command

where command is the command in question.

For online help with the gdb command while not debugging, enter

 man gdb

Testing

The two categories of software tests are:

  • black box
  • white box

Black Box Tests

The simplest type of test is a black box test.  This test is data driven.  The tester treats the program as a black box where all internal logic has been hidden from view.  The external factors alone determine the success or failure of the test.  The tests are against the specifications and input-output driven. 

The number of possibilities to be tested in a comprehensive black box test regime is often too large.  To reduce this number to a manageable set of test cases, we introduce equivalence classes. 

Equivalence Classes

Create equivalence classes using boundary values.  An equivalence class is a set where testwise any member is as good as any other (for example, i <1, =1...25, >25). 

Experience suggests that faults frequently exist at and on boundaries.  We test either side of the boundaries of the equivalence class as well as the boundary itself (for example, i = 0, 1, 2, 17, 24, 25, 26). 

We use this approach for output equivalence classes also.

White Box Testing

The complementary test to a black box test is a white box test.  White box testing is logic driven, rather than data driven.  We treat the program as a glass box where all internal logic is visible.  The internal structure determines the success or failure of the test.  Each white box test is path-oriented. 

In white box testing, we execute each possible path through the code at least once.  The number of paths may be too large to test.  To reduce this number and yet cover all paths through the code at least once, we rely on flow graphs. 

Flow Graphs

A flow graph represents the sequences, selections and iterations in a program.  Each flow graph node represents one or more sequence statements.  The edges (or arrows) between the nodes represent the flow of control. 

Consider the following code.  The flow graph is shown on the right:

 // Testing - Flow Graph
 // flowGraph.c

 #include<stdio.h>

 int main(void)
 {
     int total, value, count;

     // Start Node 1 ---
     total = 0;
     count = 0;
     // End   Node 1 ---
     do {
         // Start Node 2 ---
         scanf("%d", &value);
         // End   Node 2 ---
         if (value < 0) {
             // Start Node 3 ---
             total -= value;
             count++;
             // End   Node 3 ---
         } else if (value > 0) {
             // Start Node 4 ---
             total += value;
             count++;
             // End   Node 4 ---
         }
     // Start Node 5 ---
     } while (value != 0);
     // End   Node 5 ---
     if (count > 0)
         // Start Node 6 ---
         printf("The average value is %.2lf\n", 
                (double)total/count);
         // End   Node 6 ---

     // Start Node 7 ---
     return 0;
 }

Test Criteria

  • statement coverage - every elementary statement is executed at least once
  • edge coverage - every edge is traversed at least once
  • condition coverage - all possible values of the constituents of each compound condition are exercised at least once
  • path coverage - all paths from initial node to final node are traversed at least once.

Iterations

  • skip the iteration entirely
  • pass through the iteration once
  • pass through the iteration less than the specified number of times
  • pass through the iteration the specified number of times
  • pass through the iteration once more than the specified number of times

Compound Conditions

  • break into simple conditions

Exercises




   Printer Friendly Version of this Page print this page     Top  Go Back to the Top of this Page
Previous Reading  Previous: Portability Next: Data Conversions   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   

Creative Commons License