Chapter 14: Testing and Debugging Applications

Testing involves finding problems in your code; debugging consists of isolating and fixing the problems. Testing and debugging are necessary stages in the development cycle, and they are best incorporated early in the cycle. Thoroughly testing and debugging individual components makes testing and debugging integrated applications much easier.

For more information about creating an application, see Chapter 2, Developing an Application, and Chapter 13, Compiling an Application.

This chapter discusses:

Planning to Test and Debug

Typically, developers look for different levels of robustness as they are testing and debugging their applications:

  1. Running without crashing or generating error messages.

  2. Appropriate action in common scenarios.

  3. Reasonable action or error messages in a range of scenarios.

  4. Graceful recovery from unexpected user interactions.

Visual FoxPro provides a rich set of tools to help you isolate and identify the problems in your code so that you can fix them effectively. However, one of the best ways to create a robust application is to look for potential problems before they occur.

Debugging Before You Have Bugs

Studies have shown that good coding practices (using white space, including comments, adhering to naming conventions, and so on) automatically tend to reduce the number of bugs in your code. In addition, there are some steps you can take early in the development process to make testing and debugging easier later on, including:

Creating a Test Environment

The system environment that you expect an application to run in is as important as the data environment you have set up for the application itself. To ensure portability and to create an appropriate context for testing and debugging, you need to consider the following:

Hardware and Software

For maximum portability, you should develop applications on the lowest common platform you expect them to run on. To establish a baseline platform:

System Paths and File Properties

To ensure all necessary program files are readily accessible on each machine that will run your application, you might also require a baseline file configuration. To help you define a configuration baseline, answer the following questions:

Directory Structure and File Locations

If your source code references absolute paths or file names, those exact paths and files must exist when your application is installed on any other computer. To avoid this scenario, you can:

Setting Asserts

You can include asserts in your code to verify assumptions you have about the run-time environment for the code.

To set an assert

For example, you could be writing a function that expects a non-zero parameter value. The following line of code in the function alerts you if the value of the parameter is 0:

ASSERT nParm != 0 MESSAGE "Received a parameter of 0"

You can specify whether assert messages are displayed with the SET ASSERTS command. By default, assert messages are not displayed.

Seeing Event Sequences

When you see events occur in relation to other events, you can determine the most efficient place to include your code.

To track events

The Event Tracking Dialog Box allows you to select the events that you want to see.

Event Tracking Dialog Box

Note   In this example, the MouseMove and Paint events have been removed from the Events to track list because these events occur so frequently that they make it more difficult to see the sequences of the other events.

When event tracking is enabled, every time a system event in the Events to track list occurs, the name of the event is displayed in the Debug Output window or written to a file. If you choose to have the events displayed in the Debug Output window, you can still save them to a file as described in Displaying Output later in this chapter.

Note   If the Debug Output window is not open, events will not be listed, even if the Debugger Output Window box is set.

Isolating Problems

Once your testing has identified problems, you can use the Visual FoxPro debugging environment to isolate those problems by:

Starting a Debugging Session

You start a debugging session by opening the debugging environment.

To open the debugger

You can also open the debugger with any of the following commands:

DEBUG

SET STEP ON

SET ECHO ON



The debugger opens automatically whenever a breakpoint condition is met.

Tracing Through Code

One of the most useful debugging strategies at your disposal is the ability to trace through code, see each line of code as it executes, and check the values of all variables, properties, and environment settings.

Code in the Trace window

To trace through code

  1. Start a debugging session.

  2. If no program is open in the Trace window, choose Do from the Debug menu.

  3. Choose Step Into from the Debug menu or click the Step Into toolbar button.

An arrow in the gray area to the left of the code indicates the next line to execute.

Tips   The following tips apply:

If you isolate a problem when you're debugging a program or object code, you can immediately fix it.

To fix problems encountered while tracing code

When you choose Fix from the Debug menu, program execution is canceled and the code editor is opened to the location of the cursor in the Trace window.

Suspending Program Execution

Breakpoints allow you to suspend program execution. Once program execution has been suspended, you can check the values of variables and properties, see environment settings, and examine sections of code line by line without having to step through all your code.

Tip   You can also suspend execution of a program running in the Trace window by pressing ESC.

Suspending Execution at a Line of Code

You can set breakpoints in your code to suspend program execution in several different ways. If you know where you want to suspend program execution, you can set a breakpoint directly on that line of code.

To set a breakpoint on a particular line of code

In the Trace window, locate the line of code you want to set the breakpoint on and do one of the following:

  1. Position the cursor on the line of code.

  2. Press F9 or click the Toggle Breakpoints button in the Debugger toolbar.

    -or-

A solid dot is displayed in the gray area to the left of the line of code to indicate that a breakpoint has been set on that line.

Tip   If you are debugging objects, you can locate particular lines of code in the Trace window by choosing the object from the Object list and the method or event from the Procedure list.

You can also set breakpoints by specifying locations and files in the Breakpoints dialog box.

Breaking at a location

Examples of Locations and Files for Breakpoints

Location File Where execution suspends
ErrHandler
C:\Myapp\Main.prg The first executable line in a procedure named ErrHandler in Main.prg.
Main,10
C:\Myapp\Main.prg The tenth line in the program named Main.
Click
C:\Myapp\Form.scx The first executable line of any procedure, function, method or event named Click in Form.scx.
cmdNext.Click
C:\Myapp\Form.scx The first executable line associated with the Click event of cmdNext in Form.scx.
cmdNext::Click
  The first executable line in the Click event of any control whose ParentClass is cmdNext in any file.

Suspending Execution When Values Change

If you want to know when the value of a variable or property changes, or when a run-time condition changes, you can set a breakpoint on an expression.

Breaking when an expression changes

To suspend program execution when the value of an expression changes

  1. From the Tools menu in the Debugger window, choose Breakpoints to open the Breakpoints dialog box.

  2. From the Type list, choose Break when expression has changed.

  3. Enter the expression in the Expression box.

Examples of breakpoint expressions

Expression Use
RECNO( )
Suspend execution when the record pointer moves in the table.
PROGRAM( )
Suspend execution on the first line of any new program, procedure, method, or event.
myform.Text1.Value
Suspend execution any time the value of this property is changed interactively or programmatically.

Suspending Execution Conditionally

Often you’ll want to suspend program execution, not at a particular line, but when a certain condition is true.

Breaking on an expression

To suspend program execution when an expression evaluates to true

  1. From the Tools menu in the Debugger window, choose Breakpoints to open the Breakpoints dialog box.

  2. From the Type list, choose Break when expression is true.

  3. Enter the expression in the Expression box.

  4. Choose Add to add the breakpoint to the Breakpoints list.

Examples of breakpoint expressions

Expression Use
EOF( )
Suspend execution when the record pointer has moved past the last record in a table.
'CLICK'$PROGRAM( )
Suspend execution on the first line of code associated with a Click or DblClick event.
nReturnValue = 6
If the return value of a message box is stored to nReturnValue, suspend execution when a user chooses Yes in the message box.

Suspending Execution Conditionally at a Line of Code

You can specify that program execution be suspended at a particular line only when a particular condition is true.

Breaking when an expression is true

To suspend program execution at a particular line when an expression evaluates to true

  1. From the Tools menu in the Debugger window, choose Breakpoints to open the Breakpoints dialog box.

  2. From the Type list, choose Break at location if expression is true.

  3. Enter the location in the Location box.

  4. Enter the expression in the Expression box.

  5. Choose Add to add the breakpoint to the Breakpoints list.

  6. Choose OK.

    Tip   It is sometimes easier to locate the line of code in the Trace window, set a breakpoint, and then edit that breakpoint in the Breakpoints dialog box. To do this, change the Type from Break at location to Break at location if expression is true and then add the expression.

Removing Breakpoints

You can disable breakpoints without removing them in the Breakpoints dialog box. You can delete “break at location” breakpoints in the Trace window.

To remove a breakpoint from a line of code

In the Trace window, locate the breakpoint and do one of the following:

Seeing Stored Values

In the Debugger window, you can easily see the run-time values of variables, array elements, properties, and expressions in the following windows:

Seeing Stored Values in the Locals Window

The Locals window displays all the variables, arrays, objects, and object members that are visible in any program, procedure, or method on the call stack. By default, values for the currently executing program are displayed in the Locals window. You can see these values for other programs or procedures on the call stack by choosing the programs or procedures from the Locals For list.

Locals window

You can drill down into arrays or objects by clicking the plus (+) beside the array or object name in the Locals and Watch windows. When you drill down, you can see the values of all the elements in the arrays and all the property settings in objects.

You can even change the values in variables, array elements, and properties in the Locals and Watch windows by selecting the variable, array element, or property, clicking in the Value column, and typing a new value.

Seeing Stored Values in the Watch Window

In the Watch box of the Watch window, type any valid Visual FoxPro expression and press ENTER. The value and type of the expression appears in the Watch window list.

Watch window

Note   You can’t enter expressions in the Watch window that create objects.

You can also select variables or expressions in the Trace window or other Debugger windows and drag them into the Watch window.

Values that have changed are displayed in red in the Watch window.

To remove an item from the Watch window list

Select the item and choose one of the following:

To edit a watch

Seeing Stored Values in the Trace Window

Position the cursor over any variable, array element, or property in the Trace window to display its current value in a value tip.

A value tip in the Trace window

Displaying Output

The DEBUGOUT command allows you to write values in the Debug Output window to a text file log. Alternatively, you can use the SET DEBUGOUT TO command or the Debug tab of the Options dialog box.

If you aren't writing DEBUGOUT commands to a text file, the Debug Output window must be open in order for the DEBUGOUT values to be written. The following line of code prints in the Debug Output window at the time that the line of code executes:

DEBUGOUT DATETIME( )

In addition, you can enable event tracking, described earlier in this chapter, and choose to have the name and parameters of each event that occurs displayed in the Debug Output window.

Logging Code Coverage

Later in the development process, you might want to refine your code for performance and ensure that you've adequately tested the code by logging code coverage information.

Code coverage gives you information about which lines of code have been executed and how long it took to execute them. This information can help you identify areas of code that aren’t being executed and therefore aren’t being tested, as well as areas of the code that you may want to fine tune for performance.

You can toggle code coverage on and off by clicking the Code Coverage toolbar button in the Debugger window. If you toggle code coverage on, the Coverage dialog box opens so that you can specify a file to save the coverage information to.

Coverage Dialog Box

You can also toggle coverage logging on and off programmatically by using the SET COVERAGE TO command. You could, for example, include the following command in your application just before a piece of code you want to investigate:

SET COVERAGE TO mylog.log

After the section of code you want to log coverage for, you could include the following command to set code coverage off:

SET COVERAGE TO

When you've specified a file for the coverage information, switch to the main Visual FoxPro window and run your program, form, or application. For every line of code that is executed, the following information is written to the log file:

The easiest way to extract information from the log file is to convert it into a table so that you can set filters, run queries and reports, execute commands, and manipulate the table in other ways.

The Coverage Profiler application creates a cursor from the data generated in coverage logging and uses this cursor in a window for easy analysis.

The following program converts the text file created by the coverage log into a table:

cFileName = GETFILE('DBF')
IF EMPTY(cFileName)
   RETURN
ENDIF

CREATE TABLE (cFileName) ;
   (duration n(7,3), ;
   class c(30), ;
   procedure c(60), ;
   line i, ;
   file c(100))
   
APPEND FROM GETFILE('log') TYPE DELIMITED

Handling Run-Time Errors

Run-time errors occur after the application starts to execute. Actions that would generate run-time errors include: writing to a file that doesn’t exist, attempting to open a table that is already open, trying to select a table that has been closed, encountering a data conflict, dividing a value by zero, and so on.

The following commands and functions are useful when anticipating and managing run-time errors.

To Use
Fill an array with error information AERROR( )
Open the Debugger or Trace window DEBUG or SET STEP ON
Generate a specific error to test your error handling ERROR
Return an error number ERROR( )
Return an executing program line LINENO( )
Return an error message string MESSAGE( )
Execute a command when an error occurs ON ERROR
Return commands assigned to error handling commands ON( )
Return the name of the currently executing program PROGRAM( ) OR SYS(16)
Re-execute the previous command RETRY
Return any current error message parameter SYS(2018)

Anticipating Errors

The first line of defense against run-time errors is anticipating where they could occur and coding around them. For example, the following line of code moves the record pointer to the next record in the table:

SKIP

This code works unless the record pointer is already past the last record in the table, at which point an error will occur.

The following lines of code anticipate this error and avoid it:

IF !EOF()
   SKIP
      IF EOF()
         GO BOTTOM
      ENDIF
ENDIF

As another example, the following line of code displays the Open dialog box to allow a user to open a table in a new work area:

USE GETFILE('DBF') IN 0

The problem is that the user could choose Cancel in the Open dialog box or type the name of a file that doesn’t exist. The following code anticipates this by making sure that the file exists before the user tries to use it:

cNewTable = GETFILE('DBF')
IF FILE(cNewTable)
   USE (cNewTable) IN 0
ENDIF

Your end user may also type the name of a file that isn’t a Visual FoxPro table. To circumvent this problem, you could open the file with low-level file I/O functions, parse the binary header, and make sure that the file is indeed a valid table. However, this would be a bit of work and it might noticeably slow down your application. You would be better off handling the situation at run time by displaying a message like “Please open another file. This one is not a table” when error 15, “Not a table,” occurs.

You can’t, and probably don’t want to, anticipate all possible errors, so you'll need to trap some by writing code to be executed in the event of a run time error.

Handling Procedural Errors

When an error occurs in procedural code, Visual FoxPro checks for error-handling code associated with an ON ERROR routine. If no ON ERROR routine exists, Visual FoxPro displays the default Visual FoxPro error message. For a complete list of Visual FoxPro error messages and error numbers, see Help.

Creating an ON ERROR routine

You can include any valid FoxPro command or expression after ON ERROR, but normally you call an error-handling procedure or program.

To see how ON ERROR works, you can type an unrecognizable command in the Command window, such as:

qxy

You’ll get a standard Visual FoxPro error message dialog box saying “Unrecognized command verb.” But if you execute the following lines of code, you’ll see the error number, 16, printed on the active output window instead of the standard error message displayed in a dialog box:

ON ERROR ?ERROR()
qxy

Issuing ON ERROR with nothing after it resets the built-in Visual FoxPro error messaging:

ON ERROR

In skeletal form, the following code illustrates an ON ERROR error handler:

LOCAL lcOldOnError

* Save the original error handler
lcOldOnError = ON("ERROR")

* Issue ON ERROR with the name of a procedure
ON ERROR DO errhandler WITH ERROR(), MESSAGE()

* code to which the error handling routine applies

* Reset the original error handler
ON ERROR &lcOldOnError

PROCEDURE errhandler
LOCAL aErrInfo[1]
AERROR(aErrInfo)
DO CASE
   CASE aErrInfo[1] = 1 && File Does Not Exist
      * display an appropriate message
      * and take some action to fix the problem.
   OTHERWISE
      * display a generic message, maybe
      * send high priority mail to an administrator
ENDPROC

Handling Errors in Classes and Objects

When an error occurs in method code, Visual FoxPro checks for error-handling code associated with the Error event of the object. If no code has been written at the object level for the Error event, the Error event code inherited from the parent class, or another class up the class hierarchy, is executed. If no code has been written for the Error event anywhere in the class hierarchy, Visual FoxPro checks for an ON ERROR routine. If no ON ERROR routine exists, Visual FoxPro displays the default Visual FoxPro error message.

The beauty of classes is that you can encapsulate everything a control needs, including error handling, so that you can use the control in a variety of environments. Later, if you discover another error the control might encounter, you can add handling for that error to the class, and all objects based on your class will automatically inherit the new error handling.

For example, the vcr class in the Buttons.vcx class library, located in the Visual Studio …\Samples\Vfp98\Classes directory, is based on the Visual FoxPro container class.

Four command buttons in the container manage table navigation, moving the record pointer in a table with the following commands:

GO TOP
SKIP - 1
SKIP 1
GO BOTTOM.

An error could occur when a user chooses one of the buttons and no table is open. Visual FoxPro attempts to write buffered values to a table when the record pointer moves. So an error could also occur if optimistic row buffering is enabled and another user has changed a value in the buffered record.

These errors could occur when the user chooses any one of the buttons; therefore, it doesn’t make sense to have four separate error handling methods. The following code associated with the Error event of each of the command buttons passes the error information to the single error-handling routine of the class:

LPARAMETERS nError, cMethod, nLine
THIS.Parent.Error(nError, cMethod, nLine)

The following code is associated with the Error event of the vcr class. The actual code differs because of coding requirements for localization.

Parameters nError, cMethod, nLine
DO CASE
CASE nError = 13 && Alias not found
   cNewTable = GETFILE('DBF')
   IF FILE(cNewTable)
      SELECT 0
      USE (cNewTable)
      This.SkipTable = ALIAS()
   ELSE
      This.SkipTable = ""
   ENDIF
CASE nError = 1585 && Data Conflict
* Update conflict handled by a datachecker class 
   nConflictStatus = ;
      THIS.DataChecker1.CheckConflicts()
   IF nConflictStatus = 2
      MESSAGEBOX "Can't resolve a data conflict."
   ENDIF
OTHERWISE
* Display information about other errors.

   cMsg="Error:" + ALLTRIM(STR(nError)) + CHR(13) ;
      + MESSAGE()+CHR(13)+"Program:"+PROGRAM()

   nAnswer = MESSAGEBOX(cMsg, 2+48+512, "Error")
   DO CASE
      CASE nAnswer = 3   &&Abort
         CANCEL
      CASE nAnswer = 4   &&Retry
         RETRY
      OTHERWISE       && Ignore
         RETURN
   ENDCASE
ENDCASE

You want to make sure that you supply information for an error that you haven’t handled. Otherwise, the error event code will execute but won’t take any actions, and the default Visual FoxPro error message will no longer be displayed. You, as well as the user, won’t know what happened.

Depending on your target users, you might want to supply more information in the case of an unhandled error, such as the name and phone number of someone to call for help.

Returning from Error Handling Code

After the error handling code executes, the line of code following the one that caused the error is executed. If you want to re-execute the line of code that caused the error after you've changed the situation that caused the error, use the RETRY command.

Note   The Error event can be called when the error encountered wasn’t associated with a line of your code. For example, if you call a data environment’s CloseTables method in code when AutoCloseTables is set to true (.T.) and then release the form, an internal error is generated when Visual FoxPro tries to close the tables again. You can trap for this error, but there is no line of code to RETRY.