Assert Your Code!

Doug Hennig

Welcome to "Best Practices,"a new monthly column in FoxTalk. The purpose of this column is to discuss solid programming practices, with emphasis on techniques and ideas specific to FoxPro. As a way of introduction, let's look at this column from the viewpoint of three of journalism's five W's: Who, what, and why.

I'm a partner with Stonefield Systems Group Inc., a Canadian software development and consulting company based in Regina, Saskatchewan. I've been programming professionally since 1981, and I've written about a hundred applications for clients. In that time, I've made just about every kind of dumb mistake there is, but I've also learned a thing or two about good programming practices.

I've also had some writing experience. In the early 1980's, I was a contributing editor for Nibble, an Apple II magazine that focused heavily on useful utilities and programming techniques. I wrote occasional articles for several other computer magazines as well. As I became busier writing programs, I stopped writing articles. I recently picked up the writing bug again, and I've had articles published in FoxTalk and FoxPro Adviser. In addition, I was the author of The Visual FoxPro Data Dictionary in Pinnacle Publishing's The Pros Talk Visual FoxPro special report series.

What

Every month, I'll select a topic in the realm of "best programming practices in FoxPro." I'll discuss both FoxPro 2.x and Visual FoxPro because some of the concepts I want to cover aren't version-specific. However, I'll also delve into topics specific to Visual FoxPro (such as object-oriented programming) because there's a lot to be learned in that environment.

The focus of the column will be on techniques every programmer can use. To me, one of the most exciting things about our industry is that I learn something new every day. Sometimes it's a little specific thing, such as a way to return multiple values from a routine, and sometimes it's a big general thing, such as a technique to improve the readability of my code. The source of ideas seems boundless as well. I've had solutions to problems pop into my mind at two in the morning, and I've gotten really cool ideas from people who've been using FoxPro for less than a month. The day I have no more to learn about programming is the day to take up a different sport. Here are some topics I intend to discuss:

Nothing lives in a vacuum, especially a column like this. Feel free to send me any ideas or suggestions you have for topics you'd like to see covered. I'd also appreciate hearing your feedback on the ideas and techniques I present. Periodically revisiting the usefulness of an idea is a great way to keep your skills current and techniques sharp.

Why

I think the "why" of this column is fairly obvious: everyone wants to get better at what they do. Programming is such a young discipline that it's still somewhere between a science and an art. Many people in our field don't have formal training in computer science but instead come to programming from business or other backgrounds (my degree is in biochemistry). Even those that do have degrees in computer science may find the things they learned in college have limited applicability towards developing business applications in today's environment.

By examining solid programming practices, I hope to pass on some of what I've learned the hard way over the years (including the stuff I learned just yesterday), and help improve the skills of both you and I.

First "best practice"

Okay, enough background. The first "best practice" I'd like to pass on is the concept of assertions and how they can help you program defensively.

In his book Code Complete, Steve McConnell calls an assertion "an executable statement placed in code that allows the code to check itself as it runs." In other words, an assertion tests an assumption the programmer made when writing the code and alerts someone testing the code if the assumption proves to be wrong.

Here is some code from a "thermometer" routine (such as the thermometer displayed as GENSCRN generates an SPR from a screen set):


 parameters tnCount, tnMax
private lnPercent, lnBlocks
lnPercent = min(int(tnCount/tnMax * 100), 100)
lnBlocks  = (lnPercent/100) * wcols()
@ 0, 0 say replicate(chr(219), lnBlocks)

There are at least two assumptions in this code: both parameters are numeric and tnMax is non-zero. If either of these assumptions is incorrect, the program will bomb. "No problem," you think, "I'll just make sure I always pass the correct parameters to the routine." The problem, of course, arises a year later when you reuse this routine in another application or give it to another programmer for an application they're working on. Without reading the code thoroughly, it's easy to miss assumptions, such as the one I didn't mention earlier: tnCount must be less than or equal to tnMax or the bar will go past 100 percent, possibly even giving an error.

There are two ways you can handle this: document the assumptions (for example, in header comments for the code or in printed documentation) or make the program test for and handle invalid assumptions. Since no programmer I know reads documentation, the latter is definitely the safer course.

The following code is an assertion that tests for the assumptions in the thermometer routine:


 if type('tnCount') <> 'N' ;
  or type('tnMax') <> 'N' ;
  or tnMax = 0 ;
  or tnCount > tnMax
  wait window 'Improper parameters passed to ' + ;
    program()
  return
endif type('tnCount') <> 'N' ...

The advantage of using assertions is that your code not only becomes more bullet-proof, it also becomes self-documenting. An assertion both notifies someone reading the code that an assumption was made and tests to ensure the assumption is true as it's running.

Some programmers balk at writing assertions because of performance considerations. After all, once the code has been tested, why spend CPU cycles testing for conditions that'll never occur in a runtime environment? There are several answers to this concern:

In the first two cases, the logical property or variable can be set based on a field in a table or read from an .INI file or the Windows 95 registry. Flipping the assertions on or off is simply a matter of changing the source of data for the logical property or variable and rerunning the application. In the case of conditional compilation, the #DEFINE statement must be changed and the application rebuilt to change the enabling of assertions.

Here's an example of using a logical variable to determine if assertions should be tested:


 if glTesting ;
  and (type('tnCount') <> 'N' ;
  or type('tnMax') <> 'N' ;
  or tnMax = 0 ;
  or tnCount > tnMax)
  wait window 'Improper parameters passed to ' + ;
    program()
  return
endif glTesting ...

If glTesting is .F., the rest of the IF statement isn't evaluated. Here's an example using conditional compilation:


 #define clTESTING .T.
#if clTESTING
if type('tnCount') <> 'N' ;
  or type('tnMax') <> 'N' ;
  or tnMax = 0 ;
  or tnCount > tnMax
  wait window 'Improper parameters passed to ' + ;
    program()
  return
endif type('tnCount') <> 'N' ...
#endif

If clTESTING is defined as .F., the code between the #IF and #ENDIF isn't compiled into the application, and no assertions are tested.

An assertion class

In Code Complete, Steve McConnell describes using an assertion function to handle the dirty work of testing the assumption, displaying an error message, and shutting the application down. This sounded intriguing, so I wrote an assertion class in Visual FoxPro to handle most of the assertion work.

The assertion class, called AssertMgr, is stored in the MANAGERS.VCX class library. AssertMgr has four public properties:

It has just one public method: TestAssertion(), which tests an assertion and takes action if it fails. TestAssertion() takes three parameters. The first is either a string that must evaluate to a logical expression (for example, "tnMax <> 0") or a logical expression (for example, tnMax > 0; since there are no quotes around this, Visual FoxPro will evaluate it before passing the result to the TestAssertion() method). The second is optional: if the first parameter is a logical expression, you should pass as the second parameter a message to be displayed if the assertion fails (if the first parameter is a string, it's used as the failure message itself). The third parameter is optionally the name of the calling function (you can just pass PROGRAM() if you wish); if it isn't passed, TestAssertion() will figure out who called it.

Here's an example of how AssertMgr can be used:


 set classlib to MANAGERS additive
oAssert = createobject('AssertionMgr')
* use oAssert.lTestAssertions = .F. to disable
* assertion testing
if oAssert.TestAssertion('tnMax <> 0')
  * rest of the code goes here
else
  * cancel or return .F. or something like that
endif oAssert.TestAssertions ...

AssertMgr takes care of a lot of the details of assertion testing:

The TestAssertion() method currently returns .F. if the assertion fails. The program calling this method must then decide what to do: exit, cancel the application, and so forth. However, you could subclass AssertMgr so TestAssertion() itself cancels the application or calls your cleanup and exit routine. The calling routine would then simply call TestAssertion() and not worry about what it returns.

The logic of logging and displaying the assertion failure is located in the protected methods LogAssertionError() and DisplayAssertionError(). These methods can be subclassed if, for example, you want to log errors to a table instead of a text file, log additional information such as memory variables or results of DISPLAY STATUS, use your own display class instead of MESSAGEBOX(), or even e-mail a developer when an assertion fails.

The Companion Disk includes MANAGERS.VCX and TESTASRT.PRG, a program that demos the uses of AssertMgr. The following code is a slightly altered version of what the Class Browser exported for this class, formatted for publication.


 DEFINE CLASS assertmgr AS custom
  lTestAssertions = .T.
  lLogAssertions = .T.
  lDisplayAssertions = .T.
  cAssertionLogfile = "ASSERT.TXT"
  Name = "assertmgr"
  PROTECTED aerrorinfo[1]

  PROCEDURE TestAssertion

    * Test that an assertion is true.

    lparameters tuAssertion, ;
      tcMessage, ;
      tcModule
    local lcCurrTalk, ;
      llTest, ;
      lcModule, ;
      lnI

    * First ensure TALK is off.

    if set('TALK') = 'ON'
      set talk off
      lcCurrTalk = 'ON'
    else
      lcCurrTalk = 'OFF'
    endif set('TALK') = 'ON'

    * Flag that everything's OK in case we're not
    * supposed to test assertions.

    llTest = .T.
    if This.lTestAssertions

    * Test the type of the passed assertion. If it's
    * a string, try to evaluate it. If it's logical,
    * let's use it as the assertion itself. Otherwise,
    * let's treat the failure as the call to this
    * method itself.

      do case
        case type('tuAssertion') = 'C' ;
        and not empty(tuAssertion) ;
          and not isnull(tuAssertion) ;
          and type(tuAssertion) = 'L'
          llTest    = evaluate(tuAssertion)
          lcMessage = tuAssertion
        case type('tuAssertion') = 'L'
          llTest    = tuAssertion
          lcMessage = iif(type('tcMessage') = 'C' ;
            and not empty(tcMessage) ;
            and not isnull(tcMessage), ;
            tcMessage, 'No message specified.')
        otherwise
          llTest    = .F.
          lcMessage = 'Invalid call: ' + ;
            'assertion not logical'
      endcase

    * If the assertion failed, update This.aErrorInfo
    * array. If tcModule wasn't passed, get it from
    * the calling program's name.

      if not llTest
        if type('tcModule') = 'C' ;
          and not empty(tcModule) ;
          and not isnull(tcModule)
          lcModule = tcModule
        else
          lnI = 1
          do while not empty(program(lnI))
            lcModule = program(lnI)
            lnI = lnI + 1
          enddo while not empty(program(lnI))
          lcModule = program(lnI - 2)
        endif type('tcModule') = 'C' ...
        This.CreateErrorInfo(lcMessage, lcModule)

    * Log the assertion failure if necessary.

        if This.lLogAssertions
          This.LogAssertionError()
        endif This.lLogAssertions

    * Display the assertion failure if necessary.

        if This.lDisplayAssertions
          This.DisplayAssertionError()
        endif This.lDisplayAssertions
      endif not llTest

    * Restore TALK.

      if lcCurrTalk = 'ON'
        set talk on
      endif lcCurrTalk = 'ON'
    endif This.lTestAssertions

    * Return the results of the test.

    return llTest
  ENDPROC

  PROTECTED PROCEDURE CreateErrorInfo
    lparameters tcAssertion, ;
      tcModule

    * Create the lines of text for the message.

    dimension This.aErrorInfo[3]
    This.aErrorInfo[1] = 'Assertion: ' + ;
      tcAssertion
    This.aErrorInfo[2] = 'Module:    ' + ;
      tcModule
    This.aErrorInfo[3] = 'Date/time: ' + ;
      ttoc(datetime())
  ENDPROC

  PROTECTED PROCEDURE LogAssertionError

    * Log the assertion failure. If the file whose
    * name is in This.cAssertionLogFile exists, open
    * it. Otherwise, create it.

    local lnHandle
    if file(This.cAssertionLogFile)
      lnHandle = fopen(This.cAssertionLogFile, 2)
    else
      lnHandle = fcreate(This.cAssertionLogFile)
    endif file(This.cAssertionLogFile)

    * If we successfully opened or created the file,
    * log the assertion failure to it.

    if lnHandle >= 0
      = fseek(lnHandle, 0, 2)
      = fwrite(lnHandle, This.aErrorInfo[1] + chr(13))
      = fwrite(lnHandle, This.aErrorInfo[2] + chr(13))
      = fwrite(lnHandle, This.aErrorInfo[3] + chr(13) + ;
        chr(13))
      = fclose(lnHandle)
    endif lnHandle >= 0
  ENDPROC

  PROTECTED PROCEDURE DisplayAssertionError
    * Display the assertion failure.

    = messagebox(This.aErrorInfo[1] + chr(13) + ;
      This.aErrorInfo[2], 16, 'Assertion Failure')
  ENDPROC

ENDDEFINE

So now you have an approach to assertions in both FoxPro 2.x and Visual FoxPro. I suggest you try a couple of assertions on a small program or project that you're working on, and see how they work out. Once you find out how valuable they've become, it will become second nature to use them in all of your coding. It's sort of like commenting your code¾you didn't used to do it, but once you found out how important comments were, you'd never think of writing code without them. Right?

Doug Hennig is a partner with Stonefield Systems Group. He is the author of the Stonefield Data Dictionary and the Stonefield Database Toolkit, and has spoken at conferences including FoxTeach and the Great Lakes Great Database Workshop. 306-791-6820, CompuServe 75156,2326.

To find out more about FoxTalk and Pinnacle Publishing, visit their website at
http://www.pinpub.com/foxtalk/

Note: This is not a Microsoft Corporation website.
Microsoft is not responsible for its content.

This article is reproduced from the April 1996 issue of FoxTalk. Copyright 1996, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call (800)788-1900 or (206)251-1900.