第十四章:测试和调试应用程序

程序测试是指发现程序代码中的错误;程序调试是指从程序中找到每个问题,然后逐一解决。测试和调试是程序开发周期中必不可少的阶段,在程序开发的早期工作中,它们显得尤为重要。当对每个组件全面测试和调试之后,整个应用程序的测试和调试工作就十分简单了。

有关建立应用程序的详细内容,请参阅第二章“开发应用程序”和第十三章“编译应用程序”

本章内容要点:

程序测试和调试计划

典型的情况是,当程序员在测试和调试应用程序时,是在研究程序不同级别的可靠性:

  1. 运行不会导致崩溃或产生错误信息。

  2. 在一般情况下操作正常。

  3. 在一定范围内,操作合理,或者能提供适当的错误信息。

  4. 对意外的用户干扰很容易恢复。

Visual FoxPro 提供了丰富的测试和调试工具,帮助您逐步发现代码中的错误,有效地解决问题。但是,要想建立一个性能可靠的应用程序,最好的方法是及早发现潜在的错误。

调试程序以避免出错

研究表明,良好的编程习惯(如留出空白空间,添加代码注释,使用一般的命名规则等等)会相应减少代码中出现的问题。此外,在早期的开发过程中,可以采取一些必要步骤,这样可以使后面的测试和调试工作变得简单。这些步骤包括:

建立测试环境

应用程序运行的系统环境与您为应用程序本身设置的数据环境一样重要。为了保证可移植性并建立适当的测试和调试环境,您必须考虑以下几个方面的问题:

硬件和软件

为了获得最大的可移植性,您应当在预期运行的最底层平台上开发应用程序。要保证应用程序能在满足最低要求的平台上正常工作,您应该做到以下几点:

系统路径和文件属性

为了在运行应用程序的每台机器上都能够快速访问所有必需的程序文件,您可能需要确定一个基本文件配置。在定义基本配置时,请考虑下列问题:

目录结构和文件位置

如果您的源代码引用的是绝对路径或文件名,那么当应用程序安装到任何其他机器上时必须存在相同的路径和文件。若要避免这一情况,您可以:

设置验证信息

在代码中可以包含验证的内容,其作用是验证代码运行环境的假设情况。

若要设置验证的内容

例如,可以编写一个函数,该函数需要一个非 0 的参数值。如果参数值为 0 时,下面的函数代码将提醒您:

ASSERT nParm != 0 MESSAGE “接受的参数值为 0

可以使用 SET ASSERTS 命令指定是否显示提示信息。默认情况下,不显示提示信息。

查看事件发生的序列

当看到事件的发生与其他事件有关时,就可以确定代码放置的最有效位置。

若要跟踪事件

“事件跟踪”对话框用于选择想要查看的事件。

“事件跟踪”对话框

注释 本示例中,由于 MouseMove 和 Paint 事件的发生次数很频繁,很难看到其他的事件序列,所以把这两个事件从“跟踪事件”列表中移去。

激活事件跟踪后,每当“跟踪事件”列表中的一个系统事件发生时,该事件名字就会显示在“调试输出”窗口中,或者写到一个文件里。如果您选择了将事件显示在“调试输出”窗口中,仍可将它们保存在文件中,有关详细内容,请参阅本章稍后的“显示输出结果”

注释 如果没有打开“调试输出”窗口,那么尽管已经设置了 “调试输出”窗口,事件也不会列出来。

逐步发现错误

在测试中发现问题后,可以使用 Visual FoxPro 调试环境逐步找到错误,具体方法如下:

启动调试工作期

打开调试环境时,就启动了一个调试工作期。

若要打开调试器

也可以使用下面的任意命令打开调试器:

DEBUG

SET STEP ON

SET ECHO ON

只要满足了一个断点条件,调试器就会自动打开。

跟踪代码

在调试中,最有用的方法就是跟踪代码,以此观察每一行代码的运行,同时检查所有的变量、属性和环境设置值。

“跟踪”窗口中的代码

若要跟踪代码

  1. 启动一个调试工作期。

  2. 如果在“跟踪”窗口中没有打开程序,请从“调试”菜单中选择“运行”。

  3. 从“调试”菜单中,选择“单步”,或者单击“单步”工具栏按钮。

在代码左边灰色区域中的箭头表示下一行代码将要执行。

提示 请注意下列提示:

在调试程序或对象代码的时候,如果发现某一行代码有错误,可以马上进行修改。

跟踪代码时,若要修改遇到的问题

从“调试”菜单中选择“定位修改”时,将停止执行程序,然后打开代码编辑器,编辑器中的代码定位在“跟踪”窗口中光标所在的代码处。

挂起程序的执行

断点将挂起执行程序。停止了执行程序以后,就可以检查变量和属性的值,查看环境设置,也可以逐行检查部分代码,而不必遍历所有的代码。

提示 同样地,按下 ESC 键可以将正在“跟踪”窗口中运行的程序挂起。

在某行代码处将执行程序挂起

可以使用几种方法在代码中设置断点,将执行程序挂起。如果已经知道要在何处将执行程序挂起,那么可直接在该行设置一个断点。

若要在某个特定代码行设置一个断点

“跟踪”窗口中,找到需要设置断点的那一行,然后进行如下操作:

  1. 将光标放置在该代码行上。

  2. 按下 F9 键或者单击“调试器”工具栏上的“切换断点”按钮。

    – 或者 –

该代码行左边的灰色区域中会显示一个实心点,这表明在该行已经设置了一个断点。

提示 如果正在调试对象,那么,通过从对象列表中选择该对象,从过程列表中选择所需的方法程序或事件,就可以在“跟踪”窗口中找到特定的代码行。

可以在“断点”对话框中,指定所需断点的位置和文件来设置断点。

在定位处中断

有关为设置断点指定位置和文件的示例

位置 文件 在何处将执行程序挂起
ErrHandler
C:\Myapp\Main.prg 在程序 Main.prg 中名为 ErrHandler 的过程的第一个可执行代码行。
Main,10
C:\Myapp\Main.prg 在名字为 Main 程序的第十行。
Click
C:\Myapp\Form.scx 在文件 Form.scx 中,任何一个名字为 Click 的过程、函数、方法程序或事件的第一个可执行代码行。
cmdNext.Click
C:\Myapp\Form.scx 文件 Form.scx 中,在 cmdNext 控件的 Click 事件中,第一个可执代码行。
cmdNext::Click
在任意文件中,父类为 cmdNext 的任何控件的 Click 事件中,第一个可执行代码行。

值发生改变时,挂起执行程序

如果要了解何时一个变量或属性的值发生了变化,或者想知道何时运行条件发生了改变,那么可以对一个表达式设置断点。

当表达式值改变时停止

当表达式的值发生改变时,若要挂起运行程序

  1. “Visual Foxpro 调试器”窗口中,从“工具”菜单上选择“断点”,打开“断点”对话框。

  2. 从“类型”列表中,选择“当表达式值改变时中断”。

  3. 在“表达式”对话框中,输入相应的表达式。

断点表达式的示例

表达式 用于
RECNO( )
当表中的记录指针移动时,将程序挂起。
PROGRAM( )
在任意一个新的程序、过程、方法程序或事件的第一行上,将程序挂起。
myform.Text1.Value
当该属性的值由于用户交互或程序运行而发生了改变时,将程序挂起。

有条件地将程序挂起

通常的情况是:您并不想在某行处将程序挂起,只想在满足某一特定条件时才将程序挂起。

根据某个表达式将程序停止

若要在一个表达式值为真时,将程序中断

  1. “Visual Foxpro 调试器”窗口中,从“工具”菜单上选择“断点”,打开“断点”对话框。

  2. 从“类型”列表中,选择“当表达式值为真时中断”。

  3. 在“表达式”对话框中,输入相应的表达式。

  4. 选择“添加”,将断点添加到“断点”列表中。

断点表达式的示例

表达式 用于
EOF( )
当表中的记录指针移过最后一条记录时,将程序挂起。
'CLICK'$PROGRAM( )
在与 Click 或者 DblClick 事件相关的第一行代码上,将程序挂起。
nReturnValue = 6
如果一个信息框的返回值存储在 nReturnValue 中,当用户在该信息框上选择“确定”的时候,将程序挂起。

有条件地在某行代码行上将程序挂起

可以指定,只有在满足某一特定条件的时候,才在某特定行上将执行程序挂起。

如果某个表达式为真则在断点处中断

当一个表达式的值为真时,若要在某特定行上将执行的程序挂起

  1. “Visual Foxpro 调试器”窗口中,从“工具”菜单上选择“断点”,打开“断点”对话框。

  2. 从“类型”列表中,选择“如果表达式值为真则在定位处中断”。

  3. 在“定位”框中,输入适当的位置。

  4. 在“表达式”对话框中,输入相应的表达式。

  5. 选择“添加”,将该断点添加到“断点”列表中。

  6. 选择“确定”。

提示 有时在“跟踪”窗口找到某行代码,设置一个断点,然后在“断点”对话框中编辑该断点会更容易一些。为此,可将断点设置的“类型”从“在定位处中断”改成为“如果表达式值为真则在定位处中断”,然后添加该表达式。

移去断点

在“断点”对话框中,不用删除断点,就能使断点无效。在“跟踪”窗口中,可以删除“在定位处中断”类型的断点。

若要将断点从某个代码行中删除

“跟踪”窗口中,找到该断点,然后进行如下操作:

查看存储的值

在调试程序窗口中,很容易在如下窗口中看到变量、数组元素、属性和表达式的运行值:

在“局部”窗口中查看存储值

“局部”窗口会显示调用堆栈上的任意程序、过程或方法程序里所有的变量、数组、对象和对象元素。默认情况下,在“局部”窗口中所显示的是当前执行程序中的值。通过在“位置”列表中选择程序或过程,也可以查看其他程序或过程中的值。

“局部”窗口

在“局部”窗口和“监视”窗口中,单击数组或对象名称旁边的加号(+),可以查看数组或对象的下一级内容。当进入下一级时,就能够看到数组中所有的数组元素值,以及对象的所有属性设置。

在“局部”窗口和“监视”窗口中,通过选择所需的变量、数组元素或属性,然后单击“值”列,同时键入一个新值,即可改变这些变量、数组元素或属性的值。

在“监视”窗口中查看存储的值

在“监视”窗口的“监视”框中,键入任意一个有效的 Visual FoxPro 表达式,然后按下 ENTER 键。这时,该表达式的值和类型就会出现在“监视”窗口的列表中。

“监视”窗口

注释 不能在创建对象的“监视”窗口中输入表达式。

也可以在“跟踪”窗口或其他的调试程序窗口中,选择变量或表达式,然后将它们拖至“监视”窗口中。

在“监视”窗口中,那些已经改变的值会显示为红色。

若要从“监视”窗口列表中移去某项

选择该项,然后选择如下操作:

若要在“监视”窗口编辑一个项

在“跟踪”窗口中查看存储值

在“跟踪”窗口中,将光标定位到任何一个变量、数组或属性上,就会出现提示条,并显示它的当前值。

“跟踪”窗口中的值提示条

显示输出结果

DEBUGOUT 命令可将“调试输出”窗口中的值写入到一个文本文件的日志中。此外,也可以使用 SET DEBUGOUT TO 命令或“选项”对话框中的“调试”选项卡。

如果不想使用 DEBUGOUT 命令写入文本文件,则为了能够写入 DEBUGOUT 命令中的值,必须打开“调试输出”窗口。在运行下面一行代码的同时,在“调试输出”窗口中将打印出值。

DEBUGOUT DATETIME( )

此外,在“调试输出”窗口中,可以激活事件跟踪(在本章的前面已经介绍),也可以将每个发生事件的名字和参数进行有选择地显示。

记录代码的覆盖范围

在开发过程的后期,有时会需要代码覆盖范围的记录信息,以便精炼代码来提高程序的性能,同时确保已经进行了足够的测试工作。

代码覆盖范围所提供的信息是:哪些代码已经执行了,执行代码所用的时间是多少。这些信息可以帮助您确定没有执行的代码区域,因此也是没有测试的代码区域,以及您需要精炼代码的区域。

“Visual Foxpro 调试器”窗口中,通过单击“切换编辑日志”工具栏按钮,可以开启或关闭代码覆盖范围功能。如果打开,将出现“编辑日志”对话框,这样就可以指定一个文件来保存代码覆盖范围信息。

“编辑日志”对话框

使用 SET COVERAGE TO 命令,也能够通过程序开启或关闭代码覆盖范围功能。例如,在应用程序中,只需在想要研究的那部分代码前面包含如下的命令即可:

SET COVERAGE TO mylog.log

在这些需要记录覆盖范围信息的代码后面,使用如下命令可将代码覆盖范围功能关闭:

SET COVERAGE TO

当为代码覆盖范围信息指定了一个文件后,可以切换到 Visual FoxPro 主窗口,然后运行您的程序、表单或者应用程序。对每一行执行过的代码,下列信息将写入到日志文件中:

要从日志文件中提取所需信息,最简单方法是:将文件转换为表,然后即可对该表设置筛选条件,运行查询和报表,执行命令,或者使用其他方法操作表。

Coverage Profiler 应用程序根据覆盖范围记录中生成的数据创建了一个临时表,并在窗口中使用此临时表进行简单的分析。

下面的程序将代码覆盖范围的日志文件转换为表:

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

CREATE TABLE (cFileName) ;
   (duration n(7,3), ;
   class c(30), ;
   procedure c(60), ;
   line i, ;
   file c(100))

APPEND FROM GETFILE('log') TYPE DELIMITED

处理“运行时刻错误”

“运行时刻错误”是在应用程序开始执行后发生的。产生“运行时刻错误”的可能操作包括:对不存在的文件执行写入操作;试图打开已经打开的表;想要选择已经关闭的表;发生数据冲突;除数为零等等。

为了防止和解决“运行时刻错误”,下面的函数和命令很有用:

若要 请使用
使用错误信息填充数组 AERROR( )
打开“调试器”窗口或“跟踪”窗口 DEBUGSET STEP ON
产生指定的错误以测试自己的错误处理程序 ERROR
返回一个错误编号 ERROR( )
返回正在执行的代码行 LINENO( )
返回一个错误信息字符串 MESSAGE( )
当错误发生时,执行一个命令 ON ERROR
返回一些命令,这些命令指明了错误处理命令 ON( )
返回当前执行程序的名称 PROGRAM( )SYS(16)
重新执行前一个命令 RETRY
返回任意一个当前错误信息参数 SYS(2018)

预防错误

要防止“运行时刻错误”的发生,首先需要预见错误可能会在何处发生,然后针对可能发生的错误对代码进行修改。例如,下面的代码将表的记录指针移到下一个记录:

SKIP

此代码可以正常工作。但是当指针已位于最后一条记录上时,执行这行代码就会发生错误。

下面的代码考虑了这种情况,并可以避免错误:

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

另外一个示例,下面的代码显示“打开”对话框,然后允许用户在一个新工作区上打开表:

USE GETFILE('DBF') IN 0

存在的问题是,在“打开”对话框中,用户也许会选择“取消”,或者键入不存在的文件名字。下面的代码会考虑这些情况,保证用户在使用一个文件之前,该文件存在:

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

最终的用户也许会键入不属于 Visual FoxPro 表的文件名字,这时,您可以使用低级文件 I/O 函数打开这些文件,从语法上分析二进制的文件头,然后确保文件确实是一个有效的表。但是,所作的工作尽管很少,却可能显著地降低应用程序的运行速度。处理这种情况的较好方法是,如果在运行时刻发生错误 15(“不是一个表”)时,就显示一个信息框,如“不是一个表。”。

有时不能预见所有可能发生的错误,也许您也不想这样做。这时,要解决问题就需要编写一些代码,然后在运行时刻错误所属的事件中执行这些代码,以便捕获错误。

处理过程的错误

当过程中的代码出错时,Visual FoxPro 将检查与 ON ERROR 例程相关的错误处理代码。如果 ON ERROR 例程不存在,Visual FoxPro 就显示默认的错误信息。有关完整的 Visual FoxPro 错误信息和错误号码,请参阅“帮助”。

创建 ON ERROR 例程

在 ON ERROR 后面,可以包含任意有效的 FoxPro 命令或者表达式,但一般情况下,将调用一个错误处理过程或程序。

要了解 ON ERROR 如何工作,可以在“命令”窗口中键入一个不认识的命令,如:

qxy

这时会出现一个标准的 Visual FoxPro 错误信息对话框,显示“不能识别的命令谓词”。但是如果执行下面的代码,活动的输出窗口中就会显示错误号 16,而不在对话框中显示标准的错误信息。

ON ERROR ?ERROR()
qxy

执行不带任何参数的 ON ERROR,将错误信息重置为 Visual FoxPro 的内置错误信息:

ON ERROR

在表单的基本结构框架中,下面的代码描述了一个 ON ERROR 错误处理程序:

LOCAL lcOldOnError

*保存原始的错误处理程序
lcOldOnError = ON("ERROR")

*执行带有过程名的 ON ERROR 
ON ERROR DO errhandler WITH ERROR(), MESSAGE()

*错误处理例程所应用的代码部分

*重新设置原始的错误处理程序
ON ERROR &lcOldOnError

PROCEDURE errhandler
LOCAL aErrInfo[1]
AERROR(aErrInfo)
DO CASE
   CASE aErrInfo[1] = 1 &&文件不存在
      *显示合适的信息
      *采取一些手段修正错误
   OTHERWISE
      *显示一个通用的信息,诸如
      *向管理员发送优先权高的邮件
ENDPROC

处理类和对象中的错误

当方法程序代码中出错时,Visual FoxPro 将检查和该对象的 Error 事件相关的错误处理代码。如果在该对象的 Error 事件上没有代码,则将从父类或高于该类的其他类中执行 Error 事件的代码。如果在该类的层次结构中,找不到 Error 事件代码,Visual FoxPro 将检查 ON ERROR 例程。如果 ON ERROR 例程不存在,Visual FoxPro 则显示默认的错误信息。

类的优点是可以封装控件所需的所有项(包括错误处理),所以可以在各种环境下使用该控件。如果以后遇到了控件的其他错误,就可以向该类添加针对这个错误的处理程序,而且基于该类的对象会自动继承这个新的错误处理程序。

例如,在 Visual Studio …\Samples\Vfp98\Classes 目录下,Buttons.vcx 类库中的 vcr 类基于 Visual FoxPro 容器类。

在该容器中的四个按钮负责表的定位,使用如下的命令在表中移动记录指针:

GO TOP
SKIP - 1
SKIP 1
GO BOTTOM.

如果没有打开表时,用户选择了其中一个按钮,则就会出错。当记录指针移动时,Visual FoxPro 会试图将缓冲值写入一个表中,因此如果“优化行缓冲区”可以使用,同时另外的用户改变了缓冲记录的值,则会发生错误。

这些错误在用户选择任一按钮时,都有可能发生。所以,没有必要编写四个独立的错误处理方法。下面的代码是放在每个命令按钮的 Error 事件中,它们将错误处理信息传递给该类唯一的一个错误处理例程:

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

下面的代码放在 vcr 类的 Error 事件中。由于代码本地化的要求,实际的代码可能有所不同。

Parameters nError, cMethod, nLine
DO CASE
CASE nError = 13 &&没有找到别名
   cNewTable = GETFILE('DBF')
   IF FILE(cNewTable)
      SELECT 0
      USE (cNewTable)
      This.SkipTable = ALIAS()
   ELSE
      This.SkipTable = ""
   ENDIF
CASE nError = 1585 && 数据冲突
*更新由一个数据检查类处理的冲突
   nConflictStatus = ;
      THIS.DataChecker1.CheckConflicts()
   IF nConflictStatus = 2
      MESSAGEBOX "Can't resolve a data conflict."
   ENDIF
OTHERWISE
* 显示其他错误的信息。

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

   nAnswer = MESSAGEBOX(cMsg, 2+48+512, "Error")
   DO CASE
      CASE nAnswer = 3   &&退出
         CANCEL
      CASE nAnswer = 4   &&重试
         RETRY
      OTHERWISE       &&忽略
         RETURN
   ENDCASE
ENDCASE

需要确保对没有处理过的错误也能提供出错信息。否则,尽管程序会执行错误事件的代码,而不进行任何处理,而且也不会显示默认的 Visual FoxPro 错误信息。您和用户根本不知道到底发生了什么错误。

对于没有处理过的错误,根据最终用户的需要,可能需要提供更为详细的信息(例如寻求帮助的人的姓名和电话号码等)。

从错误处理代码中返回

在执行完错误处理代码后,程序继续执行引起该错误代码行的下一行代码。在更改了出错代码后,如果想重新执行该行代码,请使用 RETRY 命令。

注释 当遇到的错误和代码行无关时,调用 Error 事件。例如,当数据环境的 AutoCloseTables 属性为“真”(.T.) 时,如果在代码中调用了数据环境的 CloseTables 方法程序,然后释放该表单,当 Visual FoxPro 试图重新关闭表时,则会发生内部错误。您可能陷入此类错误,但却没有代码可使用 RETRY 命令来重新执行。