Tod means Fox

Business Intelligence, Data Warehousing, SQL, Visual FoxPro.

Archive for October, 2006


Published October 24th, 2006

Boundary Testing Parameters

Programs (method code, procedures, etc.) must be checked for invalid parameters. This type of testing falls within the Black Box Testing category. There are many good arguments to support the notion that boundary testing is essential. The counter argument is that a developer, through the analysis of business logic, form design, and database management has already handled boundary issues at higher levels and that testing within a program would be redundant. This argument is flawed in my opinion. In an age where code reuse is stressed and when multiple developers work on the same project, boundary tests become invaluable. In addition, boundary testing will catch hidden problems in the source code, saving you and your clients time and aggravation.

So now you’re on the boundary test bandwagon; let’s consider the simplicity of the task. There are actually two elements to these tests: preparing programs for out-of-bounds values and writing test scripts that will exercise these conditions. I’ll tackle the former in this article and the latter next week.

In many cases a simple IF construct will suffice at the top of the program to verify that the parameter passed is within the expected range. It is a good practice to consider the range of known good values and work from there. Using functions such as EVL, BETWEEN, INLIST, ISNULL, and EMPTY are a good way to start. But before we go any further, it is worth mentioning that not all parameters should be treated equally. Your boundary tests will need to consider the purpose of the parameter and not just its value. For example, does the parameter control the flow of the procedure (aka a “driver”) or is it data that will be inserted into a table or used in a calculation? Boundary tests should also be clever enough to account for parameters passed by reference or value.

Processing speed should not be an issue. Many of these checks are simple enough not to bring your application to a grinding halt. If a function is to be called repeatedly in a loop, then you may consider wrapping your boundary tests in pre-processor directives. You can compile different testing and productions versions quite easily using this method.

To test parameter boundaries at the top of a program, consider some of the following techniques:

*-- using the conditional if
tnNumber = IIF(BETWEEN(tnNumber,nLowValue,nHighValue),;
        tnNumber,nLowValue)
tcFormMode = IIF(INLIST(tcFormMode,"add","edit","readonly"),;
        tcFormMode,"readonly")
 
*-- using EVL() to make sure a parameter is not empty
*-- Note: The numeric value of 0 evaluates to empty
tcPara1 = EVL(tcPara1,"Processing...")
tnPara2 = EVL(tnPara2,1)
tdPara3 = EVL(tdPara3,DATE())
 
*-- using the if block
IF LEN(ALLTRIM(tcLastName)) > 20
    MESSAGEBOX("The last name is too long!")
ENDIF
 
*-- using one parameter to test another (VFP9 example with ICASE)
*-- this example checks a security parameter and returns the account
*-- number if security is 1 (an administrator), if security is 2, a
*-- masked  account will be returned. Otherwise, nothing is returned.
tcAccount = ;
    ICASE(tnSecurity=1,tcAccount,;
        tnSecurity=2,CHRTRAN(cNum,lcString,"**********"),'')
 
*-- Directories and filenames should be put through the ringer.
tcFileName = DEFAULTEXT(tcFileName,"txt")
tcFileName = FORCEEXT(tcFileName,"txt")
tcFileName = IIF(FILE(tcFileName),tcFileName,GETFILE())

Next week I will explore ways of writing test scripts to pound programs into oblivion, attempting to break them every which way. It may get bloody so perhaps it is appropriate that this entry will come out next week around Halloween!

Published October 12th, 2006

Using ASSERTS with VARTYPE to Test Parameters

I am always looking for ways to bullet-proof my code. One way to do this is to test parameter boundaries and types at the top of programs and methods to ensure that what we are getting is what is expected. Using VARTYPE with ASSERTS is a great way to get started.

PCOUNT(), according the documentation, returns the number of parameters passed to the current program, procedure, or user-defined function. If you are expecting 3 and are passed 2, you can use PCOUNT to handle the problem. PARAMETERS( ) does almost the same thing but can be thrown off by ON KEY LABELS and is reset every time a program, procedure, or user-defined function is called. In either case, relying on PCOUNT or PARAMETERS only gets you half the way there. What if the types are wrong? What if a developer sends in the correct number of parameters but in the wrong order?

VARTYPE() returns the data type of the expression passed to it. It can determine if an object was passed, a Date, or any of the other data types common to the language. If an array is passed, the first element is evaluated. In VFP9 the TYPE() command contains a second argument that you can use to check if a variable is an array. In VFP7, code using VARTYPE() will need to check for an individual element (or elements!). VARTYPE() also supports nulls and can return the data type of a null variable as well. VARTYPE( ) does not require the use of quotation marks around the variable name and is much faster than TYPE(). This makes it preferable in loops or at the top of code that may be run multiple times in succession.

So why use ASSERTS?

ASSERTS are used to verify assumptions you have about the run-time environment. They are great because they are ignored in production (when the exe is run directly from explorer). This means that you can use them in a development environment to bullet-proof your code and not suffer a significant performance hit when the code is shipped to the client (the command doesn’t disappear, it is just ignored). If performance is a major concern, stick #DEFINE ASSERT NOTE at the top of the function and your ASSERTS become comments.

There are situations that you will want to use VARTYPE without an ASSERT no matter what (an example would be in a case where the user’s action dictates the parameter types or order). I find these scenarios to be rare or at the least, easily identifiable.

Next entry, I’ll discuss some methods to boundary check parameters.

Published October 3rd, 2006

FoxPro’s Sometimes-Mysterious Replace

“The English country gentleman galloping after a fox - The unspeakable in full pursuit of the uneatable.”
-Oscar Wilde

The REPLACE command is a powerful command that allows developers to update fields in a table using the currently selected record (NEXT 1), a scope, or a set of records. It is also faster than SQL-UPDATE. The basic syntax is easy to understand, but its simplicity (REPLACE SomeFIeld with SomeValue) can get some developers into trouble – especially when no error or warning is raised.

Here are some examples:

1. REPLACE will not replace anything under the following conditions:

a. When the currently selected work area is at EOF:

CREATE CURSOR myCursor (mydecimal N(8,2) )
APPEND BLANK IN myCursor
CREATE CURSOR myEOFCursor (foo c(1))
REPLACE myCursor.mydecimal WITH 111.00
*-- Nothing happens

To fix code like this, use the “IN” clause of the REPLACE statement.

REPLACE myCursor.mydecimal WITH 111.00 IN myCursor
*-- Works like a charm

b. Likewise, the following code doesn’t do a thing. Although expected, there is no warning or error that the REPLACE failed:

CREATE CURSOR myCursor (mydecimal N(8,2) )
REPLACE myCursor.mydecimal WITH 123.00

c. Consider this seemingly harmless code:

CREATE CURSOR myCursor (some_int i , some_char c(3))
INDEX ON some_int TAG some_int
FOR x = 1 TO 100
    APPEND BLANK
NEXT
LOCATE
SCAN
    REPLACE some_int WITH RECNO() IN myCursor
    REPLACE some_char WITH ALLT(STR(RECNO())) IN myCursor
ENDSCAN

You would be surprised to see the results (only the first record is replaced). Now comment out the line “INDEX ON some_int TAG some_int” and rerun the code. The moral here has more to do with how FoxPro handles indexes than how it handles the REPLACE command. It is better to turn off indexes (or create them after a replace) when indexed field rely on the replace to populate it. Someday I may take the time to explain what FoxPro is doing behind the scenes.

2.) REPLACE will automatically round decimals that do not fit:

CREATE CURSOR myCursor (mydecimal N(8,2))
APPEND BLANK IN myCursor
REPLACE mydecimal WITH 12.119 IN myCursor
*-- This stores the value 12.12 into the cursor

3.) If the value doesn’t fit at all, VFP will truncate the decimal!

REPLACE mydecimal WITH 12345678.119 IN myCursor
*-- This stores 12345678 into the field,
*-- eliminating the decimal entirely

Additional notes regarding the REPLACE command:

1.) If updating multiple fields, do not use this syntax:

REPLACE firstname WITH "Tod" IN contact
REPLACE lastname WITH "McKenna" IN contact

The above syntax forces foxpro to execute the command twice. Use this instead:

REPLACE firstname WITH "Tod" ,;
    lastname WITH "McKenna" IN contact

2.) You may only include up to 128 statements using the above technique

Summary

To summarize, follow these rules when using REPLACE:

  1. Always use the “IN” clause!
  2. Examine decimal data and decide if boundary checks are necessary before a REPLACE.
  3. _TALLY will let you know if a REPLACE occurred. If you want to be absolutely sure that a REPLACE happened, you can try to ASSERT _TALLY>0 after your REPLACE command. This will alert you while testing your application that a REPLACE didn’t occur.

Your questions and comments are welcome!

-Tod