Special Edition Using Visual FoxPro 6
Special Edition Using Visual FoxPro 6
Chapter 23
Error Detection and Handling
- Getting Started When You Have a Problem
- Recognizing Common Coding Errors
- Modularizing Code to Minimize Errors
- Using Proper Parameter Passing
- Eliminating Multiple Exits and Returns
- Developing Libraries of Testing Routines and Objects
- Handling Corruption in Files
-
Designing a Test Plan
- Understanding Data-Driven Versus Logic-Driven Testing
- Defining Testing Techniques
- Determining When Testing Is Complete
- Creating a Test Environment
- Defining Test Cases That Exercise All Program Paths
- Defining Test Cases Using Copies of Real Data
- Documenting Test Cases
- Using Additional Testing Guidelines
- Asking Questions During Testing
- Understanding Methods for Tracking Down Errors
- Using Error Handlers
- Using Error Events in Objects
- Other Resources
Getting Started When You Have a Problem
A program with errors in it cannot solve problems, save someone
time, or make an organization more profitable. The bad news is
that you will write few, if any, programs totally error-free right
from the start. The good news is that you can learn how to detect
and remove errors from code using a process called debugging.
The first case of debugging allegedly occurred many years ago when an insect caused some tubes to fail in one of the first computers. Thus, the term debugging came into use for getting the "bugs" out of a system. Whatever the origin, the purpose of debugging in the software development process is to locate problems that cause programs to fail or produce incorrect results. Often, this process resembles a detective's investigation. You run the program and collect clues such as the following:
- Which procedures execute and in what order?
- What tables does the program open?
- What was the last record processed? Were any skipped?
- Which index controls the table order?
- Why do certain variables contain the values they do?
In fact, the more clues you collect, the more likely you will solve "The Case of the Program-Killing Error."
Recognizing Common Coding Errors
There are hundreds of possible errors you can make when writing Visual FoxPro applications, at least if the number of error messages is any indication. (Visual FoxPro has more than 700 defined error messages.) Some errors are more common than others, and some are pretty esoteric. Many are syntax-related and often result from simple typing mistakes. Others result from under-developed programming practices such as failure to check for EOF() before reading or writing a record. With so many possible errors, your common errors might not be the same as mine or your fellow programmers. In fact, every developer has a slightly different set of common errors that they seem to be constantly correcting. You might find yourself encountering your own subset repeatedly, based on the types of errors you tend to make. You might also have to do a little digging into the reasons for a particular error. What can start out as syntax problem or a data type that is not what you intend might not be invalid as far as VFP is concerned. It will attempt to run with whatever you supply, and the condition that develops might result in an error that appears to make little sense when presented to you.
| NOTE |
There are various sources within the language and its help system for gathering information about errors. The Reference section under Visual FoxPro Documentation contains an entire section on the 700 or so possible error messages: an alphabetical listing of messages, a list of those same messages in numerical order by error code, and a list of messages that supply additional information (known as error message parameters) via the SYS(2108) function. |
Unfortunately, in most cases, there is really nothing your program can do after the error has occurred. Most error conditions cannot be fixed in a live application. The best that you can hope for is to document what happened and then roll back transactions, close tables, and exit the program. The real challenge of error handling is to prevent the errors from occurring in the first place by writing proactive code that anticipates possible errors. You can also become more aware of common errors and thereby avoid them.
This section looks at a fairly representative set of common errors that I have seen while working with other developers. Maybe you will recognize a few of them in your own coding; don't worry if you do. Recognition is the first step in developing better habits that will eliminate common errors from your coding habits.
Syntax Errors
There are three major classes of errors in programming: logic, syntax, and exceptions. Of these, syntax errors are the easiest to find and correct. In fact, compiling your code is the fastest way to locate most of them, and the latest versions of Visual FoxPro have become even more stringent in their evaluation of syntax errors than earlier versions. Some errors that are syntax-related do not show up during compilation, however. Common syntax errors include
- Forgetting an equal sign or other operator in an expression:
* Missing = sign after gnTotalDue gnTotalDue pnNetDue * (1 + lnTaxRate)
- Spelling a command or function name incorrectly:
* Missing second 'A' in DATABASE OPEN DATBASE ptofsale
| TIP |
The VFP color-coded editor is quick to pick up spelling errors. As you type in the Command window or in your program, VFP shows reserved keywords in color. If you misspell a command, argument, or clause, it changes to normal black text. Make sure that you configure Tools, Options, Syntax Coloring to show Keywords in a color you will notice, and that you check Syntax Coloring in the Edit, Properties, Edit Properties dialog box. For more information on these settings, see Chapter 1 "Quick Review of the Visual FoxPro Interface." |
- Mispairing quotes around strings. This includes the common error of including the same type of quote inside a string as is used to delimit the string:
'This is Bill's statement' && Fails "This is Bill's statement" && Succeeds
- A variation on this error puts a single quote at one end of the string and a double quote on the other end.
| TIP |
Once again, the VFP color-coded editor can help you out. Setting up strings to appear in some jazzy way can help keep you from getting caught by mismatched quotes. I like to show strings highlighted by a background color, such as yellow or light green. This lets me know immediately if I fail to terminate a string properly. |
| TIP |
Visual FoxPro recognizes 'single quotes,' "double quotes," and [brackets] as string delimiters. If you need to use any of these characters inside the string, pick a different delimiter. |
- Mispairing parentheses in complex expressions:
REPLACE pnboxno WITH PADL(VAL(pnboxno+1,'0',5)
| TIP |
You can pick up some of these errors by clicking Verify in the Expression Builder dialog box, which is available through the design surfaces. The Expression Builder won't be able to evaluate UDFs or references to fields in unopened tables, however. |
- Using a reserved word for a memory variable or field name. Words Visual FoxPro uses for commands, functions, and keywords should not be used as variable names. This includes the use of four-character variable names that match the first four characters of a reserved word. When this happens, Visual FoxPro might try to interpret the variable or field name as the equivalent command. Usually, the command is out of context, resulting in an error. For example, the following statement generates the error Invalid use of a Visual FoxPro function as an array:
DIMENSION DATE[10]
- Not matching CASE ENDCASE, DO ENDDO, FOR ENDFOR, IF ENDIF, or SCAN ENDSCAN commands, or using the wrong terminator (ending a DO structure with ENDIF). Visual FoxPro calls this problem a nesting error:
IF gnTotalDue > 100
= MESSAGEBOX("Get a supervisor's signature")
ENDDO && ENDDO should be ENDIF
A common syntax error not found during compilation is the passing of variables of the wrong type to a procedure. Because Visual FoxPro is not a strong typed language, memory variables can assume any variable type during execution. Therefore, VFP has no way to determine during compilation that the calling program will pass a numeric value when the procedure is expecting a character string. In any case, the program fails with a syntax error when this procedure call is run.
| NOTE |
Although most of the chapter refers to compilation occurring as a separate step, you can have VFP compile your program code (.PRG) every time you save it. This option can be set in the Edit Properties dialog box discussed in Chapter 1 Code stored as methods in forms, classes, and DBCs is compiled when these windows are closed or saved, regardless of any preference settings. |
When you run a program, Visual FoxPro converts the source code to object code, if not previously compiled, and in the process detects references to other programs, procedures, and functions. It attempts to resolve these references within the current file or in other files included in the project. Suppose you attempt to call procedure SOMETHIN. If Visual FoxPro cannot resolve it, it interrupts the compilation to display the following message:
Unable to find Proc./Function SOMETHIN
VFP also displays four buttons labeled Locate, Ignore, Ignore All, and Cancel.
If you choose Locate, VFP displays an Open dialog box that enables you to find the missing file. Note, however, that this solves the problem only for the current compile. (Well, maybe. Suppose that you have a file with the same name in more than one directory or on more than one server drive on a network system. Can you be sure that you or your users will select the correct file? I think not!) If you recompile the program later without correcting this reference, either within the code or by adding the file to the Project Manager, you get another error.
Sometimes, you can ignore one or more errors during compilation if you know that the referenced procedure or function exists as a separate file or if it is an external reference. This often occurs when the compiler confuses an array reference with a function call. This problem can often be fixed by adding an EXTERNAL ARRAY arrayname in the program.
| Resolving External References |
EXTERNAL can be used to resolve references to classes, forms, labels, libraries, menus, procedures, queries, reports, screens, and tables. The most common reason for requiring EXTERNAL to resolve the reference is because the program code uses macro expansion to define the object. The following code shows an example of memory variables required to determine which of three queries and reports to open. The EXTERNAL command is used to resolve these references to the compiler. |
You can also choose to note, but ignore, unresolved references to permit VFP to complete the compile and log syntax errors into an error file. To log errors, choose Tools, Options, and the General tab. Select the option Log Compilation Errors. (You can also use the command SET LOGERRORS ON.) Visual FoxPro then writes errors to a separate text file with the root name of the compiled file and an extension of .ERR. After compiling, you can open Project, Errors to see this error file (if you are compiling from the project). If you compile individual programs, you can view the error log by typing the following:
MODIFY FILE filename.ERR
If you do not log the errors, VFP merely shows the number of errors in the status bar at the end of the compile. This is not a very informative solution, especially because this message disappears after a few seconds. Information reported with each syntax error in the log includes the following:
- The program line that caused the error
- A line number where FoxPro determines that the error exists
- An error message
When you recompile the corrected program, Visual FoxPro automatically removes this file as the program compiles without error.
| NOTE |
An important point about the line number is that it does not always point to the line containing the error. For missing parentheses or operators, the line number points to the exact line of the error. For a missing or mismatched loop terminator (for example, ENDDO, ENDIF, ENDSCAN, or ENDFOR), the line number will probably be the end of the procedure. Visual FoxPro might not realize that an error exists until the procedure or program ends and the loop has not terminated properly. |
| Always SET LOGERRORS ON |
Closing a method-editing window in a form object also performs a syntax check. For syntax errors discovered during a compile, the alert box displays three options. You can Cancel the compile and return to the code to correct the problem. You can Ignore the error and check the balance of the code-until it finds another error or reaches the end of the code. Finally, you can Ignore All, which saves the code with all its errors. If you log errors, however, you can check the error log file to list the errors and resolve them one at a time. |
If Visual FoxPro finds an error during execution that it does not find during compilation, it displays four options: Cancel, Suspend, Ignore, and Help. Cancel, of course, cancels the program. Suspend stops execution of the program and lets you enter the debugger to get more information about the error. You can also use the Command window to print values of memory variables or table fields to determine the problem. The Ignore button lets you ignore the problem; however, this is seldom a good choice. It might get you past this particular error, but chances are good that the application's memory has been compromised. Finally, the Help button opens VFP Help to give you more information about the error message.
There are some syntax errors that Visual FoxPro cannot detect until the program executes. These errors can result from mixing the order of parameters, passing the wrong number of parameters, or other similar problems. They are usually fairly easy to detect and fix. The following code lines show some runtime syntax errors:
- Mixing the order of parameters passed to a function. Visual FoxPro recognizes this error only if the switched parameters have a different type. It then responds with the Invalid function argument value, type, or count error. Mixing parameters of the same type generates logic errors at runtime.
? TRANSFORM('$$$,$$$.99', gnTotalDue) && Parameters are switched
- Entering an incorrect parameter to a command that has a limited number of possible values. For example, the ON KEY LABEL command expects specific key names. Anything else generates an error, but only at compile time, as would be the case with the following statement:
ON KEY LABEL FJ9 ZOOM WINDOW PROPERTIES NORM FROM 0,0 ; TO 10,10
- Using the wrong number of parameters or no parameters in FoxPro functions. Passing too few parameters to a UDF is not automatically flagged as an error, but the procedure might fail if it attempts to use the missing parameters because VFP initializes them to .F.. This could result in a type error in subsequent statements. On the other hand, if you pass too many arguments to a procedure, you get the following error:
Must specify additional parameters
- This might sound confusing, but look at it from the point of view of the procedure, which needs additional parameters to complete the request. The following code line attempts to use the LEFT() function to display a portion of a character string, but it does not pass the number of desired characters. This statement must fail because VFP will not know how long a string to return:
?LEFT(lcLastName) && Number of characters to return is required
- Entering a comma instead of a period, or vice versa. The difference between these two characters is hard to see on many monitors. For example, the following statement fails because a period rather than a comma separates two parameters:
LPARAMETERS lnErrNum. lcErrMessage
- This problem is made worse by the fact that these two characters occur next to each other on the keyboard. But, they are not the only character pairs likely to cause problems. Another commonly confused pair includes the zero and uppercase O. Again, both characters occur close to each other on the keyboard and look almost identical on the screen. Another frequently confused pair is the number one and lowercase l.
This list represents some common syntax errors, but not all of them. As you develop programs, you might recognize other common syntax errors of your own.
Logic Errors
Logic errors are the second major category of errors. These errors are more difficult to detect and resolve. Compiling the program does not locate them. Sometimes, a serious logic error stops a program from executing, but not always; for example, referencing an array, or one of its elements, that does not exist results in a logic error. Perhaps the program defined the array in another procedure, but gave it a private scope rather than global. When VFP encounters any variable that does not exist, it stops the program's execution.
A similar type of logic error occurs when you overwrite existing variables when restoring variables from a memory variable file or a table. The RESTORE FROM command enables a program to use variables previously stored to a .MEM file. The following code line not only restores the .MEM file variables, it also erases all current memory variables. SCATTER MEMVAR performs a similar function when retrieving fields from a table and saving their values into memory variables. If a prior memory variable had the same name as a field in the table, its value will be replaced by a corresponding field in the current record.
RESTORE FROM Savevars.mem
or
USE CUSTOMER SCATTER MEMVAR
If your program uses similar code and attempts to reference a previously defined variable, it could fail or at least generate incorrect results. The RESTORE FROM command actually causes more of a problem because it wipes out all prior memory variables before loading the ones from file. Fortunately, RESTORE FROM supports a clause to restore memory variable values without losing those already in memory. Simply include the ADDITIVE clause, as shown in the following snippet:
RESTORE FROM Savevars.mem ADDITIVE
You can still overwrite an existing variable that also exists in the .MEM file, but you won't lose the uniquely named ones.
Some logic errors create obviously wrong results. For example, you might see a string of asterisks in a field (****), which indicates a field overflow. In this case, either the field is too small or the value calculated is too large. Of course, errors do not have to be this dramatic. If a sales report calculates the total tax due as greater than the net cost of the items purchased, there is a problem. This error should be obvious to anyone who simply compares the sales totals before and after tax.
On the other hand, some errors are not obvious at all without an independent check of the calculations. Suppose that you use the following equation to calculate the number of acres in a square plot of land:
gnTotalAcres = (pnFront * pnSide) / 9 / 4480
This equation is valid and does not generate an error when executed. It multiplies the length of the front in feet by the length of the side in feet to get the area in square feet. It then divides by 9 feet per square yard. It divides the number of square yards by 4,480 rather than 4,840, however. Merely looking at the result might not reveal the fact that two digits were transposed. This type of error can go unnoticed for a long time.
Another difficult logic error to find occurs when using REPLACE. Suppose you try to change a field in a different table while the record pointer in the current table points to EOF. This error is subtle, as shown in the following code:
SELECT CUST IF !SEEK(m.pcCustId) REPLACE ORDER.cCustId WITH 'NONE' ENDIF
You might be surprised to find that this code fails. When FoxPro searches for the customer ID in table CUST and does not find it, it leaves the record pointer at the end of CUST. Even though the REPLACE statement clearly uses an alias to replace cCustId in table ORDER, the default scope of NEXT 1 actually refers to the current work area, which is CUST, not ORDER. Because the record pointer in the current work area points to EOF, REPLACE fails. Obviously, the solution here is to precede REPLACE with a SELECT Order statement.
| NOTE |
This "feature" has haunted FoxPro developers since its earliest versions. Visual FoxPro now fixes the condition with a new IN clause to the REPLACE command, which scopes the command to that other work area. You must specifically name the other work area, not just use it as an alias. Change the above code to the following.REPLACE Order.cCustID WITH 'NONE' IN Order |
Because VFP doesn't require that you qualify a field name with its alias, it's sometimes easy to forget that you are working with table fields instead of memory variables. You assign values to memory variables with an equal sign, but you put data into fields with REPLACE. Using names from the previous example, the following line will not fail, but it also won't give you the results you are looking for. VFP simply assumes you want to store the value into a variable:
cCustID = 'NONE'
Exception Errors
Exception errors, the third type of coding errors, occur due to circumstances outside of the program's direct control. For example, a program may fail because it cannot find a file it needs. Perhaps the file has been deleted or moved. Of course, if someone or some process deleted the file, there is no way for the program to continue. Even if someone has merely moved the file, you might not want to make the user responsible for finding it.
Said another way, you might not want users roaming around the file server looking for a "likely" table. First, they do not know where to look; second, they might open the wrong file, causing even more problems. Yet an expert user such as yourself might be very qualified and able to perform this search. Even experts, however, make mistakes. In any case, your program should use the FILE() function to determine whether a file exists before attempting to use it. Then, based on the user level (assigned elsewhere in your application), the code in Listing 23.1 shows one way to determine how to continue when the file does not exist. You might consider always calling a shutdown procedure when a file is not found by a program as protection against the ever-present possibility of accessing the wrong file. Alternatively, you might find some other suitable way of returning the user to the application without accessing the code that's looking for the missing file.
Listing 23.1 23CODE01.PRG-A Basic Way of Dealing with a File-Not-Found Exception
IF FILE('\VFP6BOOK\DATA\MYFILE.DBF)
* File found, open it
USE \VFP6BOOK\DATA\MYFILE.DBF
ELSE
* File not found.
WAIT WINDOW 'File: \VFP6BOOK\DATA\MYFILE.DBF NOT Found!' + ;
CHR(13) + 'Press any key to continue'
* Can this user search for it?
IF gnUserLevel > 1
lcNewFile = GETFILE('DBF', 'Find MYFILE.DBF', 'SELECT')
IF !EMPTY(lcNewFile)
USE (lcNewFile)
ELSE
DO SHUTDOWN && Or run another part of the application
ENDIF
ELSE
DO SHUTDOWN && Or run another part of the application
ENDIF
ENDIF
The previous code can very easily be converted to a generalized form and made into a function that you can call from any program or procedure. Listing 23.2 shows one possible implementation.
Listing 23.2 23CODE02.PRG-A Generalized Function for Dealing with a File Not-Found Exception
IF FILE('\VFP6BOOK\DATA\MYFILE.DBF)
* File found, open it
USE \VFP6BOOK\DATA\MYFILE.DBF
ELSE
* File not found.
lcGetFile = FINDFILE('MYFILE.DBF')
ENDIF
*** REST OF PROGRAM CONTINUES
FUNCTION FINDFILE
LPARAMETER lcFileNam
* Tell user what is happening.
WAIT WINDOW 'File: &lcFileNam. NOT Found!' + ;
CHR(13) + CHR(10) + 'Press any key to continue'
* Can this user search for it?
IF gnUserLevel > 1
lcNewFile = GETFILE('DBF', 'Find '+lcFileNam, 'SELECT')
IF !EMPTY(lcNewFile)
USE (lcNewFile)
ELSE
DO SHUTDOWN && Or continue in another part of the application
ENDIF
ELSE
DO SHUTDOWN && Or continue in another part of the application
ENDIF
RETURN lcNewFile
Another type of exception occurs when an index does not exist. If the table is part of a database, the program could retrieve the definitions for that table's indexes from the .DBC file. It could then attempt to create the missing index. The potential problem here is that indexing requires an exclusive lock on the file. If the program cannot obtain that lock, the program should shut down gracefully.
Not all exception errors are as easy to deal with. In some cases, the best alternative might be to use an ON ERROR routine to document the system at the time of the error, roll back pending transactions, close all tables, and cancel the program.
| NOTE |
ON ERROR executes a single command when Visual FoxPro encounters any error condition. That command typically uses DO to execute an error-handling procedure. ON ERROR still provides a global mechanism for handling errors in Visual FoxPro. |
The rest of this chapter shows additional methods of avoiding errors when possible, and how to track down errors that inevitably sneak in anyway.
Modularizing Code to Minimize Errors
The number of errors in any program or procedure tends to increase as it grows in size. The reason is that it becomes increasingly difficult to remember all the details in the code. The demands on software grow daily as well. Very quickly, every programmer realizes that writing large single programs with hundreds or thousands of code lines causes more problems than several smaller programs that call one another. Obviously, the more errors, the greater the amount of time spent finding and removing them. On the other hand, dividing code into smaller functional units called procedures or functions reduces overall errors. It soon becomes obvious that each procedure or function should handle no more than one task. This process is known as code modularization.
Visual FoxPro facilitates code modularization. Any application can be thought of as a series of tasks. Tasks consist of data-entry screens, reports, or procedures that manipulate data. Menu choices provide access to each task. Each task often has two or more subtasks.
How do you go about breaking a task down into smaller units or subtasks? Think of a customer form as a single task. Within it, individual data fields represent subtasks. Individual pieces of information on that form might hold the customer's name, address, or telephone number. Visual FoxPro represents subtasks as control objects. Looking deeper into a control, you find individual methods that respond to various events. Object methods are used to accomplish the following:
- Set default values
- Determine a user's rights to a field
- Create pick lists
- Validate field entries
- Save or retrieve data
- Respond to mouse clicks or moves
- Display related forms
- Send messages to other objects instructing them to alter their own characteristics, or run some method they are responsible for
See the chapters in Part IV, "Object-Oriented Programming," for more information on interobject communication.
You have already seen how to generate forms, where each form represents a block of code functionally independent of the rest of the application. Within a form, methods further modularize the code needed to handle individual object events. You can create and store the code needed for a method in a variety of ways.
First, when you open a method in Visual FoxPro's form design mode, you can enter code directly. Although this approach works, it has some drawbacks. Mainly, it limits the use of that code to one method in one form. In order to use a similar routine in another form, you might resort to cut-and-paste. You could easily end up with the complicated task of maintaining many slightly different versions of the same routine in a variety of places. You could instead store the code in a separate file. The event method then simply references it with a user-defined function (UDF) call. That's the easy part. The hard part is deciding where to store this separate code. Again, several possibilities exist.
| NOTE |
In FoxPro 2.x, form generation produced real code that you could view and use. In VFP, the process is quite different, and there is no actual code. You can pretend that you are generating code as you did in FoxPro 2.x by directing the Class Browser to produce a listing for you. However, a VFP form is simply an .SCX file in either design mode or run mode. This difference, plus application of object-oriented principles, affects how you share coded routines among your application's objects. |
In FoxPro 2.x it was a commonly accepted practice to use a separate .PRG with the same name as the form to store the needed procedures and functions used by that form. This .PRG was also responsible for calling the form.
You could run this .PRG directly, or have the application's main menu call this form-launching program. Although not the recommended object-oriented approach, this methodology can be adapted to use with Visual FoxPro, which would then have no trouble finding the functions stored in the main program that calls the form.
This technique worked well in FoxPro 2.x because it enabled quicker editing of the code and did not require the form designer just to change the code for a method. However, it is not in the OOP spirit to code this way. It does not enable sharing of code between objects with similar methods in different forms. This is a big negative when you begin to develop class libraries. The UDF call will be inherited when you subclass from the base object, but the source UDF might not be available.
Another technique stores all procedure code in a single, separate file. This file establishes a library of procedures and functions. Then, any screen with a similar method, even a subclassed screen, can call a common function. This technique makes changing the code easier. If you need a new property or method, you add it in one place. Place the reference to the code in the appropriate method of the base class and place the code in the procedure library. If more than one form uses the same method code, it still appears in one place, not everywhere the object appears.
| NOTE |
A variation of this traditional concept is to turn a procedure library into a class, where each custom method is a commonly used routine. Simply drop this library object onto your application objects to make those methods available. There can be performance tradeoffs in referencing class methods versus functions in procedure files, so this technique might be better used to group more specialized routines related to a common activity. |
With VFP objects, you can build a subclass of the original class and include common method code in its definition. By encapsulating the method's code in the definition of the class itself, you don't have to worry about separate code libraries. By sharing the object through a class library, you still code it only once. This approach works best when many forms use the same object with the same methods. An example might be VCR buttons used to control movement through a table's records.
| NOTE |
Before you store objects as custom classes in a class library, make sure to test them and all their associated code thoroughly. You can create a simple form and place the control on the form to test it. After you are satisfied with it, select the object and choose File, Save As Class. The resulting Save Class dialog box enables you to assign a class name and library file and save just the selected controls without the form. Debugging and modifying code after storing it in a class library requires more steps. |
The point to this discussion is that object methods force you to divide code into individual tasks that are more easily comprehended and maintained. The theory is that if individual tasks work properly, the sum of the tasks works properly.
Of course, you can achieve many of these same benefits in standard code by using procedures and functions that accomplish a single task. If a procedure performs more than one task, it can probably be split into two or more procedures.
Code modularization at the task level leads to another benefit: reusability. This has been mentioned before in terms of objects and object libraries, but it applies just as strongly to other common code tasks. If you write a procedure to handle movement through a table's records using a set of VCR buttons, you can use that same procedure with every table. The code remains unchanged. Developing this concept further, consider developing a library of common procedures to use in any application. You benefit from time saved by not having to write and test the same code repeatedly. Your applications also exhibit a consistent look and feel.
| TIP |
To learn more about using an object-oriented approach in your applications, see Part IV, "Object-Oriented Programming." |
Using Proper Parameter Passing
Another common error occurs during planned and unplanned parameter passing. First you might wonder what unplanned parameter passing means. Remember that Visual FoxPro makes any variable defined as a public or private variable automatically available to routines it calls. This can unintentionally redefine a variable when calling a lower-level routine that uses a variable with the same name. Visual FoxPro will not flag this as an error. After all, it assumes that you intended to do it.
As a general rule, never use the same variables in programs that call one another or in different procedures in the same program. If you must use a variable in a called procedure, pass the parameter by value and assign it a local name in the called procedure. Accidentally redefining variables in lower routines results in errors that are extremely difficult to find.
| TIP |
If you are not sure whether a variable has been used in a higher procedure, define the scope of the variable as LOCAL or PRIVATE in the called procedure. |
Another consideration is whether you pass parameters to procedures by value or by reference. When you pass a parameter by reference, the procedure uses the actual original variable. If the procedure changes the parameter value, the original variable's value changes also (see Listing 23.3).
Listing 23.3 23CODE03.PRG-An Example of Passing Parameters by Reference
* Default scope for these variables is PRIVATE a = 5 b = 6 c = 7 DO NewVal WITH a ? 'a = ' + STR(a,1) && After the subroutine, a contains 1 ? 'b = ' + STR(b,1) && b has been protected, still contains 6 ? 'c = ' + STR(c,1) && c has also been changed, value is now 8 PROCEDURE NewVal PARAMETER b b = 1 c = 8 RETURN
On the other hand, if you pass the parameter by value, the procedure creates a new variable to store the parameter value. It does not pass changes back to the original. The equivalent code is shown in Listing 23.4.
Listing 23.4 23CODE04.PRG-An Example of Passing Parameters by Value
a = 5 DO NewVal WITH (a) ? 'a = ' + STR(a,1) && Original value of 5 has been preserved PROCEDURE NewVal PARAMETER b b = 1 RETURN
When calling a procedure, Visual FoxPro's default method passes parameters by reference, except for any that you enclose in parentheses. However, when calling a function, the default method passes parameters by value (observe, functions enclose parameters in parentheses). To pass a parameter to a function by reference, you can do either of the following:
- Type SET UDFPARMS TO REFERENCE.
- Precede the parameter name with the @ character.
| TIP |
If you need to pass an array to a procedure or function, pass it by reference. If you attempt to pass it by value, it passes only the first element. |
| TIP |
There is a limit of 27 passed parameters to procedures and functions. |
Eliminating Multiple Exits and Returns
Not too many years ago, the "new" programming paradigm promoted the elimination of all GOTO statements. FoxPro developers have never had a problem with GOTOs because the language does not support a GOTO branch statement; the VFP GOTO command only navigates data files. Instead you use IF, CASE, and DO structures for conditional code processing and loops. As a result, few FoxPro developers today miss GOTO statements and the resulting tangled code they created.
Unfortunately, many FoxPro developers still use multiple exits from structured loops and multiple returns from procedures and functions. In almost all cases, there is no need for this practice. All that is usually required is a minor revision to the loop's logic. Examine the following code example:
PROCEDURE GetProdID
IF EMPTY(m.lcProdId)
RETURN
ELSE
< Code to test if lcProdId exists in PRODUCT.DBF >
RETURN
ENDIF
USE
RETURN
This procedure has three exit points where only one is required. A restructured version of the same code follows:
PROCEDURE GetProdID
IF !EMPTY(m.lcProdId)
< Code to test if lcProdId exists in PRODUCT.DBF >
ENDIF
USE
RETURN
Why be concerned about multiple exit and return points? First, it adds a level of unnecessary complexity to the code that makes tracing its path more difficult. But more important, it sometimes causes the program to skip critical code segments that it should execute. For example, in the earlier code, should the procedure close the current table (the USE command)? The first code example never closes the current file and the second one always does.
This illustrates another danger with multiple EXIT or RETURN commands. It is easy to orphan code, isolating it so that it never executes. The first example ends each branch of the IF statement with a RETURN. As a result, the USE statement never executes.
The EXIT command exits a DO ENDDO, FOR ENDFOR, or SCAN ENDSCAN loop prior to completing the loop based on the loop condition. For example, the following program segment loops through rental product records to find the serial number of a product still in stock. A simple SEEK finds a record that matches the product ID. Then, it must loop through all the records for that product until it finds the first one in stock.
USE RentProd ORDER ProdID
pcSerial = SPACE(4)
SEEK m.pcFindProdId
SCAN WHILE m.pcFindProdId = cProdId
IF lInStock
pcSerial = cSerial
EXIT
ENDIF
ENDSCAN
Observe that this code segment has two possible exits from the SCAN loop. If it finds at least one product in stock with a specific ID, it stores its serial number in variable pcSerial and exits. Otherwise, it continues to loop until the product ID changes.
A better way to write this code eliminates the extra exit. First, you need to recognize that you cannot simply eliminate the EXIT command. This would cause the loop to read through all records of the same product ID. The net effect would be to return the last available serial number rather than the first. The following code solves this problem by adding an extra conditional test to the SCAN:
USE RentProd ORDER ProdID
pcSerial = SPACE(4)
SEEK m.pcFindProdId
SCAN WHILE EMPTY(pcserial) AND m.pcFindProdId = cProdId
IF lInStock
pcSerial = cSerial
ENDIF
ENDSCAN
Observe, in this case, that SCAN tests for an empty serial number memory variable first. If this field changes before the product ID does, an in-stock item has been found. This new condition also guarantees that the loop returns the first available serial number or a blank if there is no in-stock product.
| NOTE |
A case might be made that adding the extra condition test slows the loop and thus the extra EXIT actually improves the code performance. The programming world is loaded with tradeoffs. |
A similar case can be made for eliminating the use of EXIT in other loop structures. Thus, by careful use of IF blocks and additional conditional statements, you can eliminate most, if not all, multiple exits from programs. I hope that this will also eliminate another potential source of errors in your code.
Developing Libraries of Testing Routines and Objects
Using libraries, which are program files containing common procedures and functions, is one of the most effective methods of reducing the number of errors in code. Of course, you must first thoroughly test routines before adding them to a library. Once tested, however, new applications can use them with virtual assurance that they do not contain errors.
| NOTE |
This concept of building common libraries of functions has grown up in the object world in the form of class libraries. |
In fact, Visual FoxPro's builders and wizards provide the equivalent of a standard library of creation routines. They automate the building of forms, form controls, menus, queries, reports, data tables, and the like. You do not have to write the basic code for these objects. Visual FoxPro provides error-free objects for you; all you have to do is tweak them to fit your particular application's needs.
| NOTE |
VFP 6 comes with a greater selection than ever of new and enhanced wizards, and a complete set of source code to re-create them. See Chapter 27, "The Visual FoxPro Wizards," for more information about them. Completely new additions to this version are the Foundation Classes and the Component Gallery, which provide an expanded selection of pre-built tools you can use directly in your applications. You can count on these offerings being enhanced in future versions of VFP. For more information on these new features, see Chapter 18, "The Visual FoxPro Foundation Classes" and Chapter 19, "The Visual FoxPro Component Gallery." |
You can also build your own libraries of functions and procedures to call from any program or object method. To create a library, store the functions and procedures in a single file and save it. Then, in programs that use them, simply add a SET PROCEDURE statement before referencing them. The syntax for SET PROCEDURE is
SET PROCEDURE TO [FileName1 [,FileName2,...]] [ADDITIVE]
Visual FoxPro enables programs to reference more than one library. You can even reference additional libraries later in a program by including the ADDITIVE clause. Forgetting this clause closes any prior procedure libraries when opening the new one.
Procedure libraries provide an excellent way to store and use common routines. Create common routines to open files, create sorts, display messages, and perform other common functions.
In addition to procedure files, Visual FoxPro also lets you create object libraries of your own classes. To begin, subclass one of Visual FoxPro's base classes or an existing subclass in a .VCX file. Then add additional custom properties or methods to it. For example, you could create a class consisting of a group of buttons to navigate through a file. You can then use this custom class on any form that needs file navigation buttons.
| NOTE |
It is important to distinguish between a custom class and a customized class. VFP provides a base class of type custom that comes with its own native properties, events, and methods. Any subclass, however, can be customized with the addition of user-defined properties and methods. |
To open and use a class library, use SET CLASSLIB. Its syntax is
SET CLASSLIB TO ClassLibraryName [ADDITIVE] [ALIAS AliasName]
As with SET PROCEDURE, you can have multiple class libraries open at the same time if you open each one with the ADDITIVE clause. Omitting this clause closes any previously opened class libraries.
Handling Corruption in Files
Data files can easily become corrupted. Because data files often are larger than the amount of RAM memory in a machine, Visual FoxPro constantly moves some of the files between memory and disk. Normally, everything works fine. If, however, the user turns off the computer without properly exiting the program, the file might be incompletely written. (This could be his frustrated reaction when your program fails without a proper error handling routine, and leaves tables open.) Unfortunately, the best protection against this sort of action and its resultant data corruption is a hardware UPS (uninterruptible power supply). You can take some precautions in your applications, though, primarily by tracking errors when they do occur.
In Chapter 1 "Quick Review of the Visual FoxPro Interface," an example code called a procedure, REALQUIT, to prevent the user from accidentally exiting FoxPro by clicking the wrong Close box. It proposed running a program called RealQuit when Visual FoxPro enters its ON SHUTDOWN event. Alternatively, you can add the ON SHUTDOWN command to any program to prevent the user from exiting Visual FoxPro or Windows prematurely. The procedure in Listing 23.5 should be called by ON SHUTDOWN. It includes some additional commands to properly close down Visual FoxPro. If called from an error handler, you could even build a procedure consisting of the core code inside the IF ENDIF block.
Listing 23.5 23CODE05.PRG-A Procedure for Properly Closing Down VFP
PROCEDURE SHUTDOWN
* Include any commands in this routine needed to return the system
* to a default environment. Applications running interactively
* require more attention than compiled standalone applications.
* Standalone applications might merely need to close down all
* files safely.
IF MESSAGEBOX('Do you really want to exit Visual FoxPro?', 292) = 6
* Reset common ON KEY definitions that the application may be using.
ON KEY LABEL BACKSPACE
ON KEY LABEL ENTER
ON KEY LABEL SPACEBAR
ON KEY LABEL ESCAPE
* Turn printer and file redirection off
SET ALTERNATE OFF
SET ALTERNATE TO
SET PRINT OFF
SET CONSOLE ON
* Close transaction - TRAP ERRORS
IF TXNLEVEL() > 0
ROLLBACK
END TRANSACTION
CLOSE ALL
* Release all variables (and objects)
RELEASE ALL
* Deactivate windows
DEACTIVATE WINDOWS ALL
DEACTIVATE WINDOW DEBUG
DEACTIVATE WINDOW TRACE
ACTIVATE WINDOW COMMAND
* Deactivate any application menus
SET SYSMENU TO DEFAULT
* Clear macros
RESTORE MACROS
ELSE
* Let VFP terminate
* QUIT
ENDIF
RETURN
Of course, the easiest way to fix a corrupted file is to restore a backup copy from tape or disk. If you perform daily backups, the most you lose is one day's worth of data. Even this minimal loss can be unacceptable, however. Whereas tools like dSalvage from Comtech and FoxFix from Hallogram (www.hallogram.com) worked well repairing individual tables from previous versions of FoxPro, the considerably more complex database structures in VFP require suitably sophisticated products to maintain their integrity. The best of these management and repair tools is Stonefield Database Toolkit from Stonefield Systems Group, Inc. (www.stonefield.com). Another excellent choice for database design and maintenance is xCase for Fox from RESolution Ltd. (available at www.f1tech.com). Because of the interrelated nature of database components, you need to supplement your data backup and restore procedures with some higher-level functionality that is provided by these database tools. The sorts of things you need to be able to do are as follows:
- Fix many errors in the file header.
- Recover lost memo field data.
- Reindex tables in the DBC without losing persistent relationships or referential integrity code.
- Manage the database container so that table header changes, such as table names, field names, and field properties, remain synchronized with the DBC.
- Rebuild entire databases or update individual structures at your own or client sites.
| TIP |
A structural .CDX file is the easiest, safest-and therefore, most common-type of index to use and maintain. You should reserve the use of individual .IDX files for infrequently run, ad-hoc operations. |
A corrupted index can occur when someone copies a backup .DBF to the current directory without copying its indexes. In this case, changes made to the original .DBF exist in the index, but not in the copied .DBF. Thus, the index might point to the wrong record, no record, or beyond the end of the table. This is further complicated by the fact that the database container itself (the DBC) tracks information about both the table and its index file.
You can fix simple index file problems by reindexing the table; however, even REINDEX doesn't help if the index file header is corrupted. Before Visual FoxPro, when life was simpler, you could save index definitions in a separate table, a database, a data dictionary, or simply on a piece of paper. You could then delete the corrupted index files, open the .DBF, and re-create them. This simplistic approach, however, creates new headaches in a VFP database, because you can lose other information about your files, such as persistent relationships with other files and referential integrity procedures.
The DBC, although containing a record to reference each tag name in the structural compound index of the table, does not store the index expression, nor does it store information about any other index (standalone or nonstructural). This means that copying backup tables into the current directory is a good way to get them out of synch with the DBC. You can find out more about this condition by issuing the VALIDATE DATABASE command in the Command window.
| NOTE |
If your index file contains a primary key reference, it is not easy to simply delete an index file and re-create it. The problem is that the database container stores a reference to the primary key in two places, a tag record and in the properties of the table record itself. Removing the tag record from the DBC is easy. Removing the primary key reference from the table properties is not. See additional information about working with the database container in Chapter 4 "Advanced Database Management Concepts." |
Corrupted form, label, and report files present an additional
challenge. Visual FoxPro stores information for these objects
in both a .DBF-type file and a memo file. Therefore,
they are susceptible to the same types of damage and you can use
the same tools and methods to fix them. Table 23.1 shows the corresponding
file extensions for Visual FoxPro's key components.
It might be possible to edit and remove some data corruption with programs. The safest solution, however, is to keep backup copies of all program and data files on cartridge, tape, or some other removable medium that can be stored offsite.
Designing a Test Plan
Testing your application should be considered a separate and very high priority planned task in its development. If you use a project planner such as Microsoft Project, identify testing as a separate task. There are many ways to design a test plan. Some developers test only after they completely write an application. The interactive nature of Visual FoxPro, however, makes concurrent testing during development easier and more productive. The problem is that it is harder for management to track the time spent on concurrent testing. This section looks at various testing techniques and analyzes their pros and cons.
Understanding Data-Driven Versus Logic-Driven Testing
Testing an application consists of two elements: validity and coverage. Validity testing checks to see whether the application generates the expected results for a specific set of inputs. Coverage testing checks to see whether all code statements have been executed by the tests. Any code not executed could harbor a hidden bug. I will talk more about coverage testing later. First, let's examine validity testing.
There are two basic approaches to validity testing. The first approach is data-driven. It does not assume a prior knowledge of the way the program works, rather, it focuses on selecting a variety of test data sets based on a random sampling of real-world or fabricated data. It then runs the program with the data to see whether it generates the expected results.
The second approach is logic-driven and requires extensive knowledge of the program's coding. It attempts to test every path the program can execute. It also tests how the program handles data limits by using data that pushes and exceeds known physical limitations.
Each method has advantages and disadvantages. The advantages to a data-driven approach include the fact that it does not consciously or subconsciously make assumptions about the program. Often a person "assumes" that a program never behaves a certain way and therefore fails to test it completely. Frequently, the parts of the program assumed to be correct are the very ones that fail. The primary disadvantage to a data-driven approach is that there is no guarantee that the test data sets cover all program paths and loops.
A logic-driven approach overcomes the weakness of data-driven testing. When properly designed, it tests every line of code in the entire system. The obvious disadvantage is that for a major application, fully testing every line of code requires multiple data tests. It takes time to develop the necessary data sets to ensure testing of each line of code. Further, logic-driven testing using an interactive interface can be hugely time-consuming.
Defining Testing Techniques
There are almost as many techniques for testing and debugging as there are programmers. Most approaches involve variations of just a few major techniques. The first several methods described here involve the design stage of a project. The more errors found and removed during the design phase, the less expensive the overall project becomes because it results in fewer false starts and less rework of code. This translates into reduced manhours and, thus, reduced cost.
Checking design documents involves a review of forms, reports, table layouts, and relations developed during the design phase. This occurs prior to any coding.
An informal group design review involves a group of programmers, users, and designers meeting to discuss various aspects of the project. It does not require a formalized step-through of the design specifications.
Formal design inspection analyzes the critical parts of the system design and attempts to determine whether it accounts for all possible situations. It often uses decision-tree analysis diagrams to ensure coverage. Decision-tree analysis traces each major path and operation in an application and graphically displays them on paper. Due to the usual branching of options, the resulting diagram resembles the limbs of a tree.
Personal desk-checking involves reviewing code listings and walking through the process on paper (or onscreen) without actually running the program. It often requires performing hand calculations using sample data in the actual code to check the results. Some developers refer to this technique as a walkthrough; however, the term walkthrough can also apply to other review types. This technique was more popular years ago when computer time was expensive and programmers were relatively cheap. Today, the reverse is true. Thus, this method might be called on mainly to find non-trivial errors such as complex logic errors.
| TIP |
Test often and test early. The longer an error exists, the more expensive it becomes to find and remove. |
After coding begins, the focus shifts from paper and thought reviews to formal code reviews and actual physical testing. The following paragraphs describe a few of these techniques.
Formal code inspection involves careful scrutiny of critical code segments. It provides feedback about the code, standards, use of comments, variable naming, variable scoping issues, and so forth.
Modeling or prototyping uses available tools to quickly create an application shell. Its purpose is to show overall functionality of the system. With Visual FoxPro, this involves creating basic forms and reports linked together with a simple menu. Although the forms appear functional, they might not include processing of all the business rules. Similarly, reports might not include selection and sorting options. A common term for this technique today is RAD (Rapid Application Development).
Syntax checking tests the basic correctness of the code. It checks the spelling of commands and text strings, the validity of expressions, and the basic structure of commands. FoxPro does most of this during compilation, although some syntax problems become evident only when you run the program.
| TIP |
Visual FoxPro does not check spelling or syntax within strings, form captions, ToolTips, status bar text, or messages. Yet these are the most visible parts of the program to users and could reflect on the entire system. |
Unit testing exercises individual groups of statements. For example, when designing a new class definition, use unit testing to check the code associated with its methods. To perform unit testing, you must write a special program called a driver. The driver does not become part of the final code. Rather, you use it to set up the necessary program conditions, such as initializing variables, to test the code segment.
Even though you might test each procedure and function individually, their correct functioning does not guarantee that the program as a whole will work. However, it does narrow down the error possibilities. System testing specifically checks the interface connections between modules. Its purpose is to ensure that data and logic pass correctly to and from each module. This includes using proper parameter types and sizes. It can also look for unexpected changes to public or private variables redefined by a called procedure or function.
Functional testing checks that the major features of the program work as expected. When you select a report option from the menu, do you get the report you selected or another one, or perhaps even a form? If a form displays a message to press F2 for a list of possible values, it checks that the list really appears when you press F2. It does not necessarily include verification of the report results or that the list that appears when you press F2 contains the correct data.
Stress testing checks boundary conditions of the program, such as how the program responds to extreme data values. For example, if the program tracks weekly payroll, this form of testing might check what the program does if you enter 170 hours for the number of hours the employee worked this week (seven times 24 hours is only 168). Stress testing on a network concerns itself with how the program performs when multiple users run it. Can the program handle simultaneous updates to data? Do record and file locks perform correctly while minimizing the time when other users cannot access the data?
The speed of a program falls under performance testing. Visual FoxPro provides the flexibility to code most features in several ways, but all ways do not perform equally. In fact, some methods substantially outperform others. Performance testing looks for areas to improve program speed by identifying which portions of the program require the most time. Then you can try alternative programming methods to improve the speed.
| NOTE |
Visual FoxPro itself is extremely fast. There are a lot of things built into the product to enhance performance (Rushmore Technology) and to diagnose bottleneck conditions (functions like SYS(3050) to adjust memory buffer size and SYS(3054) to analyze optimization in SQL SELECT statements). But most information about what to make of all this is shared anecdotally among developers: at conferences, in technical articles, and online. Among the best ongoing sources of help on that sort of issue are FoxPro Advisor magazine, FoxTalk newsletter from Pinnacle Publishing, CompuServe's VFOX and FoxUser forums, and www.universalthread.com. Annual large- and small-scale conferences are sponsored by Microsoft, Advisor Publications, various user groups, and companies around the world. |
Finally, compatibility testing looks at how the program works in different environments. This can include different display monitor modes, different printers, and different directory structures. It can even involve the way the program displays regional or international fields such as time, dates, and money. Even the collating sequence of sorted data and the codepage of tables become important issues when internationalizing an application. Of course, the extent you need to concern yourself with these sorts of issues is determined by how widely your application will be distributed.
| NOTE |
If yours is an international application, you might consider purchasing a third-party product to handle all the language, font, currency, and data type issues. The INTL Toolkit from Steven Black Consulting (www.stevenblack.com) is such a product that isolates the cross-cultural issues into a library. You simply include this library as an object, and then go on to address the business questions of your application. |
| TIP |
Macros can be very handy in the testing process. They can be especially useful when you have a single set of commands that doesn't rely on decisions and optional logic branches. Select Tools, Macros from the system menu to bring up a dialog box for recording and managing user-defined macros. As you issue the keystrokes in your test, VFP interprets them into a code string, which you assign to a hotkey combination. In the future, typing the hotkey alone reruns the entire set of keystrokes. |
Determining When Testing Is Complete
One might successfully argue that testing is never complete until the application becomes obsolete. There are always new combinations of data. Furthermore, most programs change over time, adding a feature here or a field there. With each new feature, you introduce the possibility of errors. Program paths might change, new variables might overwrite similar variables in other routines, and other side effects might affect existing code. For this reason, it is important to go back and retest working portions when you introduce new features. This is called regression testing.
Some developers use a bug rate factor to determine when to stop debugging. In other words, they measure the number of bugs found over time. Although this gives a nice, neat, numeric way to determine when to cut off testing, several variations exist. For example, you could monitor the cost of looking for bugs compared to the cost of leaving them in. For example, the cost of leaving a spelling error in a text label is very low. However, the cost of an error that subtracts sales tax from total due rather than adding it is higher. Another error at a higher cost level is one that returns the wrong product price when selecting products. Therefore, time is allocated for testing different parts of the program based on how serious an error would be in those parts.
No matter what method you use, it is nearly impossible to determine when all bugs have been found. Declaring testing complete just to meet a schedule, however, is dangerously shortsighted. Testing should continue until everyone involved feels confident in the application's performance.
The last section defined several techniques for testing software.
The reason for so many different methods is that no one technique
is perfect. In fact, a survey by Capers Jones, published in Programming
Productivity, shows the effectiveness of the 10 most common
techniques. Table 23.2 reproduces this table.
| Checking of design documents | |||
| Informal group design reviews | |||
| Formal design inspection | |||
| Formal code inspection | |||
| Modeling/prototyping | |||
| Personal desk-checking of code | |||
| Unit testing | |||
| Functional testing | |||
| Integration testing | |||
| Field testing | |||
| All of the above (used together) |
In this table, the three columns represent the minimum number of errors, as a percentage, found using each technique, the average number found, and the maximum number. These values assume one person is assigned to debug the code using one technique. The interesting point to the preceding table is that no one method guarantees error-free code. In fact, only a combination of methods finds close to all the errors.
Glenford J. Myers performed an interesting related study. He found that every programmer approaches debugging slightly differently. As a result, even the combination of as few as two programmers in the debugging process greatly improves the overall detection rate.
Creating a Test Environment
There are two important sets of issues concerned with creating a test environment: hardware issues and people issues. When testing an application, you should test it on the same hardware configuration that you plan to implement it on. If the application will run on a network, testing it on a standalone system can miss many of the potential problems related to record and file sharing.
| NOTE |
You can simulate a multiuser environment even on standalone machines by opening multiple instances of VFP. Of course, having additional memory helps improve the performance of this technique. |
A similar problem occurs when the software must run in various display resolutions. The same "perfect" form on one display might have fields and labels that fall off the screen when run with different display options.
| TIP |
Visual FoxPro gives some help in this regard. Choose Tools, Options from the system menu. On the Forms tab, use the Maximum design area combo box to select the resolution in which your application will run. |
It is important to use real data to test the system when it nears completion. For that reason, a recommended mode of development creates and implements those modules needed to collect data first. Then users can begin using them to enter real data. This data can then serve as test data for subsequent development.
Of course, all development should occur physically separated from a live system and live data. If your new system is replacing an old one, you can continue the testing phase even after implementation by maintaining a parallel environment, including a duplicate set of live data files. This enables you to compare results from your new system against output as it is currently produced.
In spite of precautions, errors can occur during development that could destroy live data. For example, you could mistakenly enter the path of the live data rather than the test data and ZAP it. This also means that you should periodically back up even your test data and test programs.
Not everyone is good at testing. In fact, some people enjoy testing more than others. (These are often the same people who enjoy bureaucratic detail such as income tax forms.) Often the developers of an application make the worst testers. They know the program too well and tend to enter only correct data and keystrokes. They subconsciously don't want to make their own program fail, even if consciously they recognize the need for testing. They also don't necessarily approach the application the same way a user might. So, in order to improve an application's robustness, it is important to assign the task of testing to others besides those who created it.
On the other hand, other staff members might not have the emotional attachment to a program and therefore appear ruthless in their testing. Although "ruthless" testers might find more problems, they must exercise tact in discussing them with the developer. After all, a program is the product of someone's creativity, and you do not want to suppress that. Developers can assist the process by not taking it personally when testers uncover bugs. In order to succeed, testers and developers must maintain a mutual goal of making the application stronger.
Furthermore, Visual FoxPro provides many different ways to perform a task. Therefore, don't criticize a person's methods just because you might do it differently. If a better, more efficient way exists to perform a task, find a way to show the other developers how it is better. Help them to learn, don't force them to learn.
Defining Test Cases That Exercise All Program Paths
Test data can consist of white box testing or black box testing. White box testing thoroughly checks all program paths. Because it requires knowing the logic of the program, it is also called logic-driven testing. Black box testing requires no knowledge of the program logic. It just gathers test data and runs it. There is no inherent assumption that the test data actually tests all the program paths because the paths are not known.
A side benefit of creating white box test data is the need to review the program carefully to identify these paths. This process often uncovers problems even before testing begins. There are two goals with white box testing:
- Identify and test all defined paths required for the application.
- Ensure that no possible path is missing or unaccounted for by the application.
Defining Test Cases Using Copies of Real Data
Whenever programmers try to "generate" test data, they almost always, without fail, miss at least one special case. For that reason, a random sample of real data generally provides just as good a test of the real operating environment. Thus, you should try to collect real data as early in the project as possible.
The best way to do this in a completely new system is to develop data entry forms or other programs that collect data with minimal functionality as early as possible. The Form Designer makes this relatively painless. Forms should include at least minimal data validation but might not include all look-up tables, context-sensitive help, or other advanced features implemented through custom methods. Alternatively, if this is a rewrite, conversion, or extension of an existing application, you must find a way to convert existing data to the format your new modules will expect. This probably means writing a data export routine that will create new versions of files for you to work with while leaving the existing ones in place.
If the application involves several processes, the task of collecting real data becomes more complex. In a real system, the processing of data takes place continuously. Therefore, you really need to capture an instantaneous snapshot of the data. Any time delay that enables users to process data could cause the data to be unsynchronized. This causes false errors (errors due to bad data, not due to program logic) when testing the program. Significant time can be lost tracking down and resolving these "false" errors. If possible, collect a copy of live data from all tables when no one else is on the system, such as at 3:00 a.m.
Documenting Test Cases
If you just randomly run test cases, you really don't know how thoroughly they test the procedure or application. Not only do you need to know how thoroughly the cases cover all possible program paths, you need to know what each case tests. Documenting test cases provides historical case results to compare future changes to the application against.
| NOTE |
Although not specifically designed for Visual FoxPro, it is possible to create a series of scripts to test your application with Microsoft Test. |
Testing is so important that the IEEE (Institute of Electrical and Electronic Engineers) has developed standards for documenting tests. They are summarized in the following list:
- Identify the test case, its purpose, and what features it tests.
- Identify features not involved in the test.
- Identify any requirements for the test (such as data files, indexes, relations, drivers, and so on).
- Identify any additional hardware or software requirements. For example, OLE tests require other programs, such as Excel, Word, or Mail.
- Identify any assumptions made in the test (such as minimum memory, hard disk requirements, and available floppy drives).
- Identify the steps needed to run the test.
- Identify the criteria for what constitutes passing or failing the test.
- Identify where the tester might want to suspend execution to examine the program path or variable values.
- Identify any conditions that could cause the program to stop prematurely and detail what the tester should do at that point.
- Maintain a history of tests performed for each case, who performed it, the results, and any corrective action made to the code.
- Make an estimate of the amount of time needed to set up and perform the test. After running the test, document the actual times.
Using Additional Testing Guidelines
Following are some additional testing guidelines:
- Test early and test often. Basically, this means test code at the lowest level possible. If you use the Expression Builder, always click the Verify button to check the expression's syntax. If you are building custom classes, build a small driver program or form to test them before using them in an application. Test individual procedures and functions. Create macros to test interactive elements such as menu selections and form behavior.
- Create a flow chart to diagram the major program paths.
- Identify calculations that require special consideration to test more thoroughly. For example, a routine that calculates the number of workdays between any two dates has more complexity than a calculation of total days between two dates. This routine can be made even more complex if it considers the effect of holidays.
- Be able to verify test results by some other independent means. This requires either the capability to perform the calculation by hand or to compare it to other results.
- Use generalized routines and class libraries as much as possible. After these are developed and tested, you need not test them again unless you change them. This saves considerable time that you can invest in other parts of the program.
Also, examine each input field and determine whether it needs any of the following:
- A default
- A picture clause
- Special formatting
- A lookup option
- Special validation or range checking
| TIP |
If you are going to require this same type of input field in more than one place in your application, consider making it a class. |
Asking Questions During Testing
Although it might never be possible to absolutely guarantee that you have found all errors, by asking several questions about each module you write, you can minimize the number of errors. The following list provides just a few of the possible questions you might ask. Over time, you'll undoubtedly expand this list with your own questions.
- Examine the relations between fields. Do some fields require information from others, thus making the entry order important?
- Should the user be able to exit an incomplete form? If so, what happens to the data?
- What does the program do if a required program does not exist? If a required index does not exist? If a form or report does not exist?
- Where does the program expect to find data tables?
| TIP |
Remember that even if there is only one location for data, it's not a good idea to hard-code this path throughout the program. At the very least, you will want to maintain a separate location for test files. Devise a method to inform the program which path you expect it to use each time you run it. |
- Must the user define the SET commands before running the program? If so, how? For example, do you know that in VFP, many of the SET commands are scoped to the current DATASESSION? This means that each form can support its own SET environment by simply setting its DataSession property to 2 (private data session).
Understanding Methods for Tracking Down Errors
Despite all the preceding preparation to avoid common errors and the development of test sets, errors still happen. Your first priority when an error occurs is to find it and fix it. You can later go back to your test cases to determine why they didn't discover it in the first place.
When an error occurs during program execution, Visual FoxPro displays an error box with a simple error message. Beneath the message, it has four buttons: Cancel, Suspend, Ignore, and Help. Most of the time, you do not want to ignore an error. If you write programs for other users, you never want them to ignore an error. In fact, you probably don't want them to see this default error message. Rather, you want to trap all errors with your own error handler, which documents the system at the time of the error and gracefully exits the application.
As is true of any rule, however, there might be a few exceptions. If the program fails because it cannot find a particular bitmap on the system where it's currently running, you might be willing to ignore that error. Most other errors you cannot ignore. If VFP cannot locate a table, ignoring the problem does not enable the program to run anyway. It needs that table for a reason. So just document the problem and quit the program.
As a developer, you will find that Suspend is a valuable option during testing. It stops program execution without removing current variables from memory. Any tables currently open remain open with their record pointers in place. Most important, you can then open the Trace window of the debugger. The Trace window displays the source code being executed, highlighting the current line. While suspended, time stops for the application, enabling you to examine details of the program and its variables. You can even use the Trace window to execute the program line-by-line.
Visual FoxPro includes several built-in functions to provide clues when errors occur. To use them, you must interrupt the default error handling of Visual FoxPro. Rather than display a sometimes cryptic message to the user, you want to pass control to an error-handling routine. Listing 23.6 shows a simple error routine that you should consider. It displays a little more information than the standard error window by using a multiline Wait window. You could use the same approach to log errors, along with some user identification, to a file instead of onscreen. That way you can review the entries to get a better picture of how frequently, and how universally, the errors occur. The user information enables you to track down who is having the problem so that you can get more information about what they were doing at the time.
Listing 23.6 23CODE06.PRG-A Routine to Move Beyond the Standard Error Window to a More Informative Approach
ON ERROR DO ErrLog WITH ;
ERROR(), MESSAGE(), MESSAGE(1), LINENO(1), PROGRAM()
** Rest of the application **
PROCEDURE ErrLog
LPARAMETER lnErrorNo, lcMessage, lcErrorLine, ;
lcErrLineNo, lcModule
WAIT WINDOW ;
'An error has occurred in: ' + lcModule + CHR(13) + ;
'ERROR: ' + STR(lnErrorNo, 6) + ' ' + lcMessage + CHR(13) + ;
'On Line: ' + STR(lcErrLineNo, 6) + ' ' + lcErrorLine
RETURN
The ERROR() function returns a number that represents the error. The appendix in the Developer's Guide lists all error numbers along with a brief description, as does the Error Messages section of VFP's online Help.
Perhaps more useful is the MESSAGE() function. When used without a parameter, MESSAGE returns an error message associated with the error. This message is usually the same one that appears in the Error dialog box displayed by Visual FoxPro. As a bonus, MESSAGE(1) returns the program line that caused the error.
The function LINENO() returns the program line number that suspended the program. By default, this line number is relative to the first line of the main program. Because a typical application calls many procedures and functions, this value has less importance than the one returned by LINENO(1). This function returns the line number from the beginning of the current program or procedure. Use this value when editing a program by opening the Edit pull-down menu and selecting Go to Line. After entering the line number at the prompt, the editor places the insert cursor at the beginning of the error line. The default Visual FoxPro text editor can display line numbers if you check Show Line/Column Position in the dialog that appears when you select Edit, Properties. If you want to see line numbers in the Trace window, select Tools, Options, Debug and choose the Trace option button.
PROGRAM(lnLevel) returns the name of the executing program if lnLevel is 0, and the main program if it equals 1. It reports deeper call levels as the value of lnLevel increases until it returns an empty string. Visual FoxPro supports nested program calls up to 128 levels deep. This function is similar to SYS(16) except that SYS(16) also includes the path. When the error occurs in a procedure or function, SYS(16) begins with its name. Then it displays the path and the parent program name.
SYS(16,lnLevel) also supports a second parameter that tells it to print the program names at a specific level in the calling sequence. If lnLevel is equal to zero, the function returns the name of the currently executing program. An lnLevel value of 1 begins with the main program. Sequential values step through the procedure calling sequence until reaching the procedure or function containing the error. At that point, subsequent values of the parameter return an empty string. The code that follows traces the calling sequence up to the error:
lnLevel = 1 DO WHILE !EMPTY(SYS(16, lnLevel)) ? SYS(16, lnLevel) lnLevel = lnLevel + 1 ENDDO
Testing Errors While Suspended
The most obvious place to start tracking down reasons for an error is at the point where the program fails. You might not know exactly where that point is, but you probably have a good idea of which routine is giving you a problem. Maybe you see a hard crash every time you enter data into a particular field or click a certain button on the form. Or it might be more subtle than that. Maybe the crash occurs while you're closing the form, and parenthetically, saving data and cleaning up from previous tasks.
The first thing to do is run the program up to a point as close to the failure as you can get and then suspend execution. You accomplish suspension in any of a variety of ways. Many developers set up a hotkey that enables them a backdoor into debugging mode. A command such as the following enables you to suspend your program even at points that don't appear interruptible-for example, when you are in a "wait state" because the current command is READ EVENTS:
ON KEY LABEL F12 SUSPEND
If you are successful in getting to a point where things have not yet gone off track, you will have an environment with all memory variables and tables intact and open. You can then use the Command window, the View window, and the debugger to display all sorts of information to lead you to the error.
You can print things such as variable values to the screen, checking for undefined or unusual types. If you find an undefined variable, you at least know what to look for. You can examine the code preceding the current line for any clues as to why the variable has not been defined.
| TIP |
Remember to issue an ACTIVATE SCREEN command before displaying any values on the desktop. The default for commands such as DISPLAY MEMORY, DISPLAY STATUS, and ?lcVariable is to use the currently active output window, which is probably one of your application's screens. |
If all the variables look correct, you can enter and test the line that failed. If the line contains a complex expression, test portions of it to determine where it fails. If you find a simple error, it might be possible to correct and test it while you are still in the Command window.
| TIP |
Copying and pasting ensures that you don't add new syntax errors by retyping lines. |
Suppose that the expression looks correct and all variables are defined, but the line still fails. You might have the wrong syntax for a function, be missing a parenthesis, or have some other syntax error. In this case, use FoxPro's online help to check the proper syntax for any command or function.
| TIP |
To get fast help on a Visual FoxPro command or function, highlight it in the program edit or Command window and press the F1 key. |
Bring Up the Data Session Window There are a lot of things you can do while your program is suspended. You can bring up the Data Session window (known as the View window in FoxPro 2.x) and check out much of the data picture through it. You can browse files, check sort orders, inspect structure, and even open additional files.
| TIP |
If the system menu has been replaced by an application menu, you might not have access to Window, Data Session. If that's the case, simply enter SET in the Command window. |
You might be surprised to see unexpected files or even no files at all listed in the window when it first appears. Chances are you are looking at the environment for the default data session, but you have set up your form in a private data session. Check the entry in the drop-down list labeled Current Session. Pull down the list and select the one corresponding to your application form and you should see the expected files.
Tricks in the Command Window VFP provides you with some extremely powerful new SYS() commands that you can use for debugging. In particular, SYS(1270) has become the darling of many developers of my acquaintance. To use it, position the mouse over something on the screen, and type the following in the Command window:
ox = SYS(1270)
The variable ox then contains an object reference to whatever was pointed to. That object can be a running form or a control on the form, depending on just what was under the mouse. Or, the object reference might be to a class or a form or a control in design mode. You can tell exactly what that reference is by typing ?ox.Name in the Command window. If you're interested in something further up or down the containership hierarchy, reposition the mouse and reissue the SYS(1270) command.
So, why is this useful? After you have a reference to some object, you can find out anything you want about that object. You can check its ControlSource property, for example, to make sure you're getting data from the right place. Maybe you notice that the object just isn't behaving as you expect. Try displaying ox.ParentClass or ox.ClassLibrary to make sure you're working with the intended version. Better yet, you can change any enabled properties on the object, or fire events, simply by typing appropriate lines into the Command window, like this:
ox.AutoCenter = .T. ox.Hide()
| TIP |
SYS(1270) is another command you might want to attach to a hotkey. Save yourself having to type it every time you want to change the object reference by instead typing the following line once: |
Break Down Complex Commands
Sometimes an error occurs in a section of code that you cannot test separately. BROWSE and SELECT statements often extend over many lines. When Visual FoxPro reports an error in these commands, it cannot tell you which line within the statement contains the error. For these long commands, it can be difficult to quickly spot the error. The SELECT statement that follows illustrates this situation with a complex SELECT that includes several tables as well as a union between two separate SELECTs:
SELECT A.cStoreId, A.cTicket, A.cItemId, A.nQuantity, ;
A.nUnitPrice, A.nExtPrice, ;
B.cEmplId, B.cCompanyId, B.cDeptNo, B.cBatch, ;
B.dDate AS DATE, ;
C.cCenterNo, D.cLastName, ;
LEFT(E.cProdDesc,25) as ProdDesc, ;
G.cCoName, H.cDeptName ;
FROM TKTDETL A, TICKET B, CENTERS C, CUSTOMER D, ;
PRODUCT E, COMPANY G, DEPARTMT H ;
WHERE A.cStoreId+A.cTicket = B.cStoreId+B.cTicket AND ;
A.cStoreId+A.cTicket+A.cItemId = ;
C.cStoreId+C.cTicket+C.cItemId AND ;
&FiltStr1 ;
UNION ALL ;
SELECT A.cStoreId, A.cTicket, A.cItemId, A.nQuantity, ;
A.nUnitPrice, A.nExtPrice, ;
B.cEmplId, B.cCompanyId, B.cDeptNo, B.cBatch, ;
B.dDate AS DATE, ;
SPACE(10) AS cCenterNo, D.cLastName, ;
LEFT(E.cProdDesc,25) as ProdDesc, ;
G.cCoName, H.cDeptName ;
FROM TKTDETL A, TICKET B, CUSTOMER D, ;
PRODUCT E, COMPANY G, DEPARTMT H ;
WHERE A.cStoreId+A.cTicket = B.cStoreId+B.cTicket AND ;
A.cStoreId+A.cTicket NOT IN ;
(SELECT F.cStoreId+F.cTicket from CENTERS F) AND ;
&FiltStr2 ;
INTO CURSOR MPA
Debugging a SELECT statement this complex is difficult without taking advantage of another technique called code reduction or decomposition. The purpose of code reduction is to reduce the amount of code used in a test. In this case, an obvious first attempt at reducing the test code is to split the individual SELECT statements and test each one separately. After you determine which SELECT statement causes the error, you can remove additional code from it until it finally works. With a SELECT, further reduction can mean removing one table at a time along with its related fields and relations. Or, you could begin by removing sorts or groupings. With any method you choose, at some point the SELECT statement begins to work. It is then a relatively simple matter to determine what is wrong with the code just removed.
After finding and correcting an error, you can proceed in several ways. You could take a pessimistic approach and incrementally rebuild the SELECT one table or feature at a time making sure it continues to work. Or you could take an optimistic approach and test the changes in the original complete SELECT statement. Of course, there are levels in between. In this case, you might want to test the individual SELECT statement with the correction before copying it back into the union.
You can apply this same approach to any code, not just single commands like SELECT. Take any program or procedure that does not work and comment out functionality until it does. Whatever you mark as comments probably contains the error.
Clues in the Code
A review of recent program changes provides another good clue to an error's cause. The most likely place to look when an error occurs in an existing program that ran fine previously is in any recent code changes. This is not a guarantee. After all, the error might reside in a code path that was not executed before. However, it is a good place to begin.
This brings up another point. When you make changes to your programs, it's a good idea to leave the old code in place for reference, at least until you're certain that the replacement code runs correctly. Of course, you don't want the program to run both the old and the new code. You should insert a comment identifying the nature of the change and comment out the old code.
| NOTE |
Although you can comment out individual program lines by adding an asterisk in front of each one, this can become tedious for blocks of code. As a shortcut, select the lines of code you want to comment by clicking the mouse at the beginning of the first line and dragging through the lines to highlight everything you want to skip. Then choose Format, Comment from the system menu or right-click and choose Commen |
Adding Wait Windows or Other Printed Output
Not all errors point to obvious lines of code. Sometimes a logic error originates in an entirely different section of the code from where the program finally fails or displays erratic behavior. In these cases, you need to spot check the program's activity at various points.
One way to check a program's path is to embed WAIT statements throughout the code using the following format:
WAIT WINDOW 'Beginning of PROCEDURE COPYDATA' NOWAIT
Every time execution passes a statement like this, a Wait window appears in the upper-right corner. The NOWAIT option enables the program to continue without pausing. (If you want the program to pause, skip the NOWAIT option or use the MESSAGEBOX() function with only the first argument to display a message.) Adding WAIT statements has an additional advantage for programs that perform extensive background processing. Even when used in a production environment, they assure the user that the program is still executing. Users who think a program has stopped might reboot the computer, which can lead to data corruption.
You can also halt the program at any point with the SUSPEND command or add commands to print or list information about the program such as those shown in Listing 23.7.
Listing 23.7 23CODE07.PRG-Commands to Output Information About the Current Program State
lcMemFile = 'MEM' + LEFT(CTOD(DATE()),2) + ;
SUBSTR(CTOD(DATE()), 4, 2) + '.TXT'
lcStatFile = 'STAT' + LEFT(CTOD(DATE()),2) + ;
SUBSTR(CTOD(DATE()), 4, 2) + '.TXT'
LIST MEMORY TO FILE &lcMemFile
LIST STATUS TO FILE &lcStatFile
Of course, you don't want these commands to execute every time a user runs the program. If you enter them as shown previously, there is a high risk that you will forget and leave them in the final user version. Because users do not need to see this output, you might want to bracket these commands with an IF ENDIF such as the following:
IF glDebugMode
<< Place any debug command here >>
ENDIF
You could then initialize the variable glDebugMode at the beginning of the program to turn these commands on or off. By using a variable and defining it in the main program, you need to change and compile only one routine to turn these statements on. A better technique stores the value for glDebugMode outside the program, perhaps in a configuration table or memory variable file. Then you can activate the debug mode without having to recompile the system at all.
Even when these commands are inactive, they require memory, both for the additional program lines and the memory variable. An alternative uses the #DEFINE and #IF #ENDIF directives to include or exclude the debug code at compile time.
#DEFINE glDebugMode .T. #IF glDebugMode << Place any debug commands here >> #ENDIF
Asserts
A variation on the idea of displaying information in windows throughout the application is to use a feature that was introduced in VFP 5. Instead of WAIT WINDOW or MESSAGEBOX() calls, code ASSERT commands at strategic points using this syntax:
ASSERT lExpression MESSAGE cMessageText
When the program encounters this command, it evaluates the expression you've coded. If the expression is True, execution continues. If it evaluates to False, VFP displays your message in a dialog box with the options Debug, Cancel, Ignore, and Ignore All. Your message is optional. If you omit it, VFP issues a default: Assertion failed on line n of procedure cProcedureName.
There's a catch. In order to get VFP to test your assertions, you must issue SET ASSERTS ON. Maybe you've