发布新日志

  • 简单说说我理解的性能测试(转)

    2007-09-30 13:56:36

    关于性能测试,
    我觉得现在很多情况下,很多人误解了它存在的意义。
    刚才和kernzhang聊天,说到性能测试调优的问题。
    性能测试来说,调优,我想是很重要的一块。
    而如果在现有架构的基础上调优。
    只能说是牺牲这个得到那个。
    不可能兼而得之。

    于是我写了一段自己对性能测试的理解,记录下来:

    有一个误区是,很多人把性能测试看成是找到系统性能BUG的途径,而对性能测试来说,我认为找到应用程序或者数据库等的BUG,是性能测试中不得已而为之的一部分。
    我们做测试知道,测试什么,应该有一个预期的结果。
    而在实际性能测试的过程中,写场景或者用例的时候,很多时间我发现很多人都没有考虑这一点。换句话说,有很多人并不知道预期是什么。
    从而让找BUG成了性能测试的一项重要的工作。
    拿一个简单的比方来说:我希某个应用在现有的硬件和架构下达到,每秒处理1000笔业务。而我们测试中发现,只能处理500笔。那么哪里消耗了时间。我们找到了最后,发现是某个语句的循环消耗了不应该消耗的时间。
    那么这个问题,是我们性能测试找到的。但是这是我们的目的吗?我认为,我只是性能测试的一个份量比较小的目的。
    那我再从配置测试上来说,如果排除代码的效率(此效率应该是性能的一部分,不过应该在前期的白盒测试中去做)。我们能做的就是把现有的配置,搞清楚,然后用消耗其中的一种资源,而得到其他资源的充分利用,或者响应时间的提升。即,我们已经没有办法把性能提高到特别好的层次上了。因为架构和硬件已经决定了。
    而有时我们做稳定性测试的时候,发现有些应用在长时间的压力下出现了各种各样的问题。比如连接没有释放。这样的问题又回到了上面的找BUG的说法上。所以配置测试是调优的很重要的部分。
    比如,我们可能会增加JVM或者连接数来达到比较好的性能,
    而让系统充分利用硬件的多余资源。
    再说一下群集的测试。
    我们知道,群集的测试和只有一个web application server和一个DB server的两层应用、或者是再加一个中间件的三层应用不同。在这里,我们还要考虑的是当其中的一个结点down掉的时候,其他结点是不是能够快速的接管服务并提供正确的服务。我们知道有些群集应用,用内存镜像来做的,这个的问题是,可能在内存中出现的问题,会同步到其他的结点上,从而所有的结点都会有问题。
    当然也有厂商提供了更好的群集方案和硬件配置。我们做这样的测试和一般的性能测试不同的是,模拟某个结点出现问题,而其他结点要多久才能接管服务。是不是在用户感觉不到的情况下,已经接管了?这里我们要监控的东西就更多了。要分析的东西也就更多了。
    我要说的做性能测试希望做到的,就是让整个应用在现有的配置和架构下,达到最好的效率,以提供最优的服务。而不要把性能测试理解到其他的用途上去。

    先写这些吧,可能有些散乱或者不对的地方,请指正。 



    Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1685126


  • LoadRunner完全卸载方法??

    2007-09-29 09:02:58

    LoadRunner完全卸载方法


    如何重新安装LoadRunner:

    如果安装LoadRunner最新版本失败,相信很多朋友都会遇到重新安装不成功的烦恼。原因可能是多种情况,可能是早期的LoadRunner版本兼容性问题导致安装失败,也可能安装过程中弹出组件注册失败的各种错误。如果正常重新安装,只能先让LoadRunner充分卸载。

    可以按以下的步骤操作:

    1.保证所有LoadRunner的相关进程(包括Controller、VuGen、Analysis和Agent Process)全部关闭。

    2.备份好LoadRunner安装目录下测试脚本,这些脚本一般存放在LoadRunner安装目录下的“scrīpts”子目录里。

    3.在操作系统控制面板的“删除与添加程序”中运行LoadRunner的卸载程序。如果弹出提示信息关于共享文件的,都选择全部删除。

    4.卸载向导完成后,按照要求重新启动电脑。完成整个LoadRunner卸载过程。

    5.删除整个LoadRunner目录。(包括Agent Process)

    6.在操作中查找下列文件,并且删除它们
    1) wlrun.*
    2) vugen.*

    7.运行注册表程序(开始- 运行- regedit)

    8.删除下列键值:
    如果只安装了MI公司的LoadRunner这一个产品,请删除:
    HKEY_LOCAL_MACHINESOFTWAREMercury Interactive.
    HKEY_CURRENT_USERSOFTWAREMercury Interactive.
    否则请删除:
    HKEY_LOCAL_MACHINESOFTWAREMercury InteractiveLoadRunner.
    HKEY_CURRENT_USERSOFTWAREMercury InteractiveLoadRunner.

    9.最后清空回收站

    如果你完成了以上操作,你就可以正常的重新安装LoadRunner。最好保证安装LoadRunner时关闭所有的杀毒程序。因为以往在安装LoadRunner时同时运行杀毒程序会出现不可预知的问题。
     
    8.删除下列键值:
    如果只安装了MI公司的LoadRunner这一个产品,请删除:
    HKEY_LOCAL_MACHINESOFTWAREMercury Interactive.
    HKEY_CURRENT_USERSOFTWAREMercury Interactive.
    否则请删除:
    HKEY_LOCAL_MACHINESOFTWAREMercury InteractiveLoadRunner.
    HKEY_CURRENT_USERSOFTWAREMercury InteractiveLoadRunner.


    键值如果删除了,注册码就填不进去了,还是先把这相关的键值导出来
    我把这些值删了之后就再也装不上注册码,最后还是恢复系统才把它搞定了
     
     
  • QTP中常有的VB函数

    2007-09-20 17:37:54

    Left 函数
            返回 Variant (String),其中包含字符串中从左边算起指定数量的字符。

    语法

    Left(string, length)

    Left 函数的语法有下面的命名参数:

    部分     说明
     
    string  必要参数。字符串表达式其中最左边的那些字符将被返回。如果 string 包含 Null,将返回 Null。
     
    length  必要参数;为 Variant (Long)。数值表达式,指出将返回多少个字符。如果为 0,返回零长度字符串 ("")。如果大于或等于 string 的字符数,则返回整个字符串。
     

    说明

    欲知 string 的字符数,使用 Len 函数。

    注意    LeftB 函数作用于包含在字符串中的字节数据。所以 length 指定的是字节数,而不是要返回的字符数。

    Mid 函数
            从字符串中返回指定数目的字符。

    Mid(string, start[, length])

    参数

    string

            字符串表达式,从中返回字符。如果 string 包含 Null,则返回 Null。

    Start

            string 中被提取的字符部分的开始位置。如果 start 超过了 string 中字符的数目,Mid 将返回零长度字符串 ("")。

    Length

            要返回的字符数。如果省略或 length 超过文本的字符数(包括 start 处的字符),将返回字符串中从 start 到字符串结束的所有字符。

    说明

            要判断 string 中字符的数目,可使用 Len 函数。

            下面的示例利用 Mid 函数返回字符串中从第四个字符开始的六个字符:

    Dim MyVar

    MyVar = Mid("VB脚本is fun!", 4, 6) 'MyVar 包含 "scrīpt"。

    注意   MidB 函数与包含在字符串中的字节数据一起使用。其参数不是指定字符数,而是字节数。

     

    Len 函数
            返回字符串内字符的数目,或是存储一变量所需的字节数。

    Len(string | varname)

    参数

    string

            任意有效的字符串表达式。如果 string 参数包含 Null,则返回 Null。

    Varname

            任意有效的变量名。如果 varname 参数包含 Null,则返回 Null。

    说明

            下面的示例利用 Len 函数返回字符串中的字符数目:

    Dim MyString

    MyString = Len("VBscrīpt") 'MyString 包含 8。

    注意   LenB 函数与包含在字符串中的字节数据一起使用。LenB 不是返回字符串中的字符数,而是返回用于代表字符串的字节数。

     

    Right 函数
            从字符串右边返回指定数目的字符。

    Right(string, length)

    参数

    string

            字符串表达式,其最右边的字符被返回。如果 string 参数中包含 Null,则返回 Null。

    Length

            数值表达式,指明要返回的字符数目。如果为 0,返回零长度字符串;如果此数大于或等于 string 参数中的所有字符数目,则返回整个字符串。

    说明

            要确定 string 参数中的字符数目,使用 Len 函数。

            下面的示例利用 Right 函数从字符串右边返回指定数目的字符:

    Dim AnyString, MyStr

    AnyString = "Hello World"      ' 定义字符串。

    MyStr = Right(AnyString, 1)    ' 返回 "d"。

    MyStr = Right(AnyString, 6)    ' 返回 " World"。

    MyStr = Right(AnyString, 20)   ' 返回 "Hello World"。

    注意   RightB 函数用于字符串中的字节数据,length 参数指定返回的是字节数目,而不是字符数目。

     

    InStr 函数
            返回某字符串在另一字符串中第一次出现的位置。

    InStr([start, ]string1, string2[, compare])

    参数

    start

            可选项。数值表达式,用于设置每次搜索的开始位置。如果省略,将从第一个字符的位置开始搜索。如果 start 包含 Null,则会出现错误。如果已指定 compare,则必须要有 start 参数。

    string1

            必选项。接受搜索的字符串表达式。

    string2

            必选项。要搜索的字符串表达式。

    compare

            可选项。指示在计算子字符串时使用的比较类型的数值。有关数值,请参阅“设置”部分。如果省略,将执行二进制比较。

    设置

            compare 参数可以有以下值:

    常数
     值
     描述
     
    vbBinaryCompare
     0
     执行二进制比较。
     
    vbTextCompare
     1
     执行文本比较。
     

    返回值

    InStr 函数返回以下值:

    如果
     InStr 返回
     
    string1 为零长度
     0
     
    string1 为 Null
     Null
     
    string2 为零长度
     start
     
    string2 为 Null
     Null
     
    string2 没有找到
     0
     
    在 string1 中找到 string2
     找到匹配字符串的位置
     
    start > Len(string2)
     0
     

    说明

    下面的示例利用 InStr 搜索字符串:

    Dim SearchString, SearchChar, MyPos

    SearchString ="XXpXXpXXPXXP"   ' 要搜索的字符串。

    SearchChar = "P"   ' Search for "P".

    MyPos = Instr(4, SearchString, SearchChar, 1)   ' 在位置 4 进行的文本比较。返回 6。

    MyPos = Instr(1, SearchString, SearchChar, 0)   ' 在位置 1 进行的二进制比较。返回 9。

    MyPos = Instr(SearchString, SearchChar)   ' 默认情况下,进行的是二进制比较(省略了最后的参数)。返回 9。

    MyPos = Instr(1, SearchString, "W")   ' 在位置 1 进行的二进制比较。返回 0(找不到 "W")。

    注意   InStrB 函数使用包含在字符串中的字节数据,所以 InStrB 返回的不是一个字符串在另一个字符串中第一次出现的字符位置,而是字节位置。

     

    LTrim、RTrim与 Trim 函数
            返回不带前导空格 (LTrim)、后续空格 (RTrim) 或前导与后续空格 (Trim) 的字符串副本。

    LTrim(string)

    RTrim(string)

    Trim(string)

    string 参数是任意有效的字符串表达式。如果 string 参数中包含 Null,则返回 Null。

    说明

            下面的示例利用 LTrim, RTrim, 和 Trim 函数分别用来除去字符串开始的空格、尾部空格、 开始和尾部空格:

    Dim MyVar

    MyVar = LTrim("   vbscrīpt ")   'MyVar 包含 "vbscrīpt "。

    MyVar = RTrim("   vbscrīpt ")   'MyVar 包含 "   vbscrīpt"。

    MyVar = Trim("   vbscrīpt ")   'MyVar 包含 "vbscrīpt"。

     

    Rnd 函数
    返回一个随机数。

    Rnd[(number)]

    number 参数可以是任意有效的数值表达式。

    说明

            Rnd 函数返回一个小于 1 但大于或等于 0 的值。number 的值决定了 Rnd 生成随机数的方式:

    如果 number 为
     Rnd 生成
     
    小于零
            每次都相同的值,使用 number 作为种子。
     
    大于零
            序列中的下一个随机数。
     
    等于零
            最近生成的数。
     
    省略
            序列中的下一个随机数。
     

            因每一次连续调用 Rnd 函数时都用序列中的前一个数作为下一个数的种子,所以对于任何最初给定的种子都会生成相同的数列。

            在调用 Rnd 之前,先使用无参数的 Randomize 语句初始化随机数生成器,该生成器具有基于系统计时器的种子。

            要产生指定范围的随机整数,请使用以下公式:

    Int((upperbound - lowerbound + 1) * Rnd + lowerbound)

            这里, upperbound 是此范围的上界,而 lowerbound 是此范围内的下界。

    注意   要重复随机数的序列,请在使用数值参数调用 Randomize 之前,立即用负值参数调用 Rnd。使用同样 number 值的 Randomize 不能重复先前的随机数序列。

     

    Randomize 语句
    初始化随机数生成器。

    语法

    Randomize [number]

            可选的 number 参数是 Variant 或任何有效的数值表达式。

    说明

            Randomize 用 number 将 Rnd 函数的随机数生成器初始化,该随机数生成器给 number 一个新的种子值。如果省略 number,则用系统计时器返回的值作为新的种子值。

            如果没有使用 Randomize,则(无参数的)Rnd 函数使用第一次调用 Rnd 函数的种子值。

    注意 若想得到重复的随机数序列,在使用具有数值参数的 Randomize 之前直接调用具有负参数值的 Rnd。使用具有同样 number 值的 Randomize 是不会得到重复的随机数序列的。

  • QTP关键技术(三) - 对同步点的理解

    2007-09-20 17:30:38

    QTP的脚本语言是VBscrīpt,脚本在执行的时候,执行语句之间的时间间隔是固定的,也就是说脚本在执行完当前的语句之后,等待固定的时间间隔后开始执行下一条语句。会出现以下问题:假设后一条语句的输入是前一条语句的输出,如果前一条语句还没有执行完,这时候将要导致错误的发生!具体措施:加入同步点、加入Wait语句,如何实现呢?他们有什么区别呢?


     

    1)QTP的脚本语言是VBscrīpt,脚本在执行的时候,执行语句之间的时间间隔是固定的,也就是说脚本在执行完当前的语句之后,等待固定的时间间隔后开始执行下一条语句
     
    2)问题:假设后一条语句的输入是前一条语句的输出,如果前一条语句还没有执行完,这时候将要导致错误的发生!
     
    3)措施:加入同步点、加入Wait语句
     
    4)同步点Synchronization Point
    QTP脚本在执行过程中如果遇到同步点,则会暂停脚本的执行,直到对象的属性获取到了预先设定的值,才开始执行下一条脚本。
    如果在规定的时间内没有获取到预先设定的值,则会抛出错误信息。
     
    例如:
    Window("Flight Reservation").ActiveX("Threed Panel Control").WaitProperty "text", "Insert Done...", 10000
    执行到上面这条语句时,QTP会暂停执行,直到显示”Insert Done…”,
    如果在规定的时间10,000ms后text的值没有等于”Insert Done…”,则会抛出错误信息
     
    5)如何获取Synchronization Point
           A.在Recording状态下,通过Insert è Synchronization Point实现
           B.非Recording状态下,在Expert View下,通过Insert è Step Generator è Category(Test Objects)è Object(The Object you’re Testing) è Operation(WaitProperty)è PropertyName、PropertyValue、TimeOut分别填写"text", "Insert Done...", 10000
     
    6)Wait
           总的来说就是死等,比如说wait 10,当运行到这条语句时,等待10秒钟后,才开始再读下面的语句。所以说写脚本的时候一定要估计好时间,否则的话会浪费运行的时间,或者出现等待时间不足的现象。

     

  • QTP关键技术(二) - 对Check Point的较为深入理解

    2007-09-20 17:27:45

    1. 定义:
    将特定属性的当前数据与期望数据进行比较的检查点,用于判定被测试程序功能是否正确
    Check Point可以分两类:QTP内置验证点和自定义验证点
     
    2. QTP内置验证点实现原理及优缺点
           A.录制时,根据用户设置的验证内容,记录数据作为基线数据
           B.回放时,QTP捕获对象运行时的数据,与脚本中的基线数据进行比较
           C.如果基线数据和运行数据相同,结果为PASS,反之为Failed.
           D.优点是 操作简单方便
           E.缺点是 QTP默认的检查的属性有时不符合自己的要求,如希望得到检查的属性没有在里面, 而默认的属性不需要检查等。
     
    3. QTP内置验证点结果的应用
           A.录制的验证点在没有进行调整前,仅仅是给出了检查结果是通过还是错误的
           B.实际的测试过程中,可以根据验证点的结果进行不同的操作
           If Window("Flight Reservation").WinEdit("Name:").Check(CheckPoint("Name:")) = True then
                  msgbox "oh, success!"
    Else
                  msgbox "oh, failure!"
    End If
     
    4. 自定义验证点的应用及优缺点
           A.使用条件语句对实际值和期望值进行对比,然后用Reporter对象报告结果
           '检查Ticket Number
    If CStr(dbTicketNumber) = CStr(DataTable("oTicketNumber", dtLocalSheet)) Then
           Reporter.ReportEvent micPass, "打开订单- TicketNumber", "期望结果是:" & dbTicketNumber & ", 界面显示实际结果是:" & DataTable("oTicketNumber", dtLocalSheet)
    Else
           Reporter.ReportEvent micPass, "打开订单- TicketNumber", "期望结果是:" & dbTicketNumber & ", 界面显示实际结果是:" & DataTable("oTicketNumber", dtLocalSheet)
    End If
           B.优点是 非常灵活,前者实现的所有检查都可以用此方法来实现;
           C.缺点是 代码量大,对测试人员的要求高。
     
    5. 对Check Point的深入理解
    摘自:51Testing,http://bbs.51testing.com/viewthread.php?tid=86742&highlight=check
    A.个人认为在比较简单的和有Active Screen的情况下可以使用QTP内置的Check Point,在比较复杂的情况下可以通过编程和使用Reporter来完成.
    B.在使用check方法时,必须先在Keyword View或者Active Screen中新建CheckPoint。否则无法对该对象进行check,系统报错说无法在对象仓库中找到此对象。如果插入检查点,系统会自动把相关的对象添加到对象库中。
    我认为检查点并不是一个实实在在的对象。因为你可以对同一个对象设置不同的检查点,可以把它的某个属性既设定成True,也可以设定为False。而对象库中的对象的属性值是必须依赖于对象的实际属性值的。如果随意更改有可能无法识别。还有就是可以针对同一个对象设定多个检查点。在测试窗口中可以看到这两个检查点的名称是区分开来的。所以我认为检查点并不是实际存在的对象,而是一些类似映射的东西。
    尽管检查点并不是对象库中的实在的对象,但是它必须对应到对象库中的某个实实在在的对象,好像它的一个映像一样,而且在实际的操作过程中,QTP还是把它作为一个对象来处理的。
    因为我们无法像其他对象一样把“检查点对象”添加到对象库中,而QTP又认为它是个对象,所以我们无法在专家视图中直接添加检查点脚本。但是我们可以采用编成描述的方式来实现检查点的功能。
    CheckPoint 是一个依赖于Object Repository(对象库)中的某个对象的“虚拟对象”。其具体含义是:如果它所依赖的QTP 对象库中的对象没有了,那么此CheckPoint 也就不存在了;这个“虚拟对象”的属性是从它所依赖的对象的属性中“抽取”出来的,它具有它所依赖的对象的一个或几个属性,但不能增加它所依赖的对象没有的任何属性。
    CheckPoint 是一个“虚拟对象”的重要原因是:每个Object都能在Object Repository找到它的Name、Class Properties,而CheckPoint 在Object Repository中就根本不存在。选择脚本中的某个对象后,在Object Property 的对话框里面有个Respository按钮,点击它后,你会看到此对象在Object Respository 的Name、Class 和 Properties。
    选择一个CheckPoint后,在CheckPoint Properties 的对话框里没有 Respository 按钮,在Object Respository中也找不到此CheckPoint的Name、Class 和 Properties(因为它在对象库中根本就不存在!)。

     

  • QTP关键技术(一) - 对象识别及存储技术基本常识

    2007-09-20 17:25:09

    什么是测试对象模型,测试对象,运行时对象?QTP的录制过程和回放过程是怎么实现的?

     

     

    1)测试对象模型(Test Object Model)
    测试对象模型是QTP用来描述应用程序中对象的一组对象类。每个测试对象类拥有一系列用于唯一确定对象属性和一组QTP能够录制的方法
     
    2)测试对象(Test Object)
    用于描述应用程序实际对象的对象,QTP存储这些信息用来在运行时识别和检查对象
     
    3)运行时对象(Run-Time Object)
           是应用程序中的实际对象,对象的方法将在运行中被执行
     
    4)QTP的录制过程
           A.确定用于描述当前操作对象的测试对象类,并创建测试对象
           B.读取当前操作对象属性的当前值,并存储一组属性和属性值到测试对象中
           C.为测试对象创建一个独特的有别于其他对象的名称,通常使用一个突出属性的值
           D.记录在对象上执行的操作
     
    5)QTP的回放过程
           A.根据对象的名称到对象存储库(Object Repository)中查找相应的对象
           B.读取对象的描述,即对象的属性和属性值
           C.基于对象的描述,QTP在应用程序中查找相应的对象
           D.执行相关的操作
  • LoadRunner函数中文翻译系列之一--Action

    2007-09-20 17:16:24

    web_url
    语法:
            Int Web_url(const char *name, const char * url, <Lists of Attributes>, [EXTRARES,<Lists of Resource Attributes>,LAST)

    返回值
            成功时返回LR_PASS (0),失败时返回 LR_FAIL (1)。

    参数:
            Name:VuGen中树形视图中显示的名称,在自动事务处理中也可以用做事务的名称。

            url:页面url地址。

            List of Attributes

            EXTRARES:分隔符,标记下一个参数是资源属性的列表了。

            List of Resource Attributes

            LAST:属性列表结束的标记符。

    说明
            Web_url根据函数中的URL属性加载对应的URL,不需要上下文。

            只有VuGen处于URL-based或者HTML-based(此时A scrīpt containing explicit URLs only选项被选中时)的录制模式时,web_url才会被录制到。

            可以使用web_url 模拟从FTP服务器上下载文件。web_url 函数会使FTP服务器执行文件被真实下载时的操作。除非手工指定了"FtpAscii=1",下载会以二进制模式完成。

            在录制选项中,Toos—Recording Option下,Recording选项中,有一个Advanced HTML选项,可以设置是否录制非HTML资源,只有选择了“Record within the current scrīpt step”时,List of Resource Attributes才会被录制到。非HTML资源的例子是gif和jpg图象文件。

            通过修改HTTP头可以传递给服务器一些附加的请求信息。使用HTTP头允许请求中包含其他的内容类型(Content_type),象压缩文件一样。还可以只请求特定状态下的web页面。

            所有的Web Vusers ,HTTP模式下的WAP Vusers或者回放模式下的Wireless Session Protocol(WSP),都支持web_url函数。

    web_image
    语法:
            Int web_image (const char *StepName, <List of Attributes>, [EXTRARES, <List of Resource Attributes>,] LAST );

    返回值
            成功时返回LR_PASS (0),失败时返回 LR_FAIL (1)。

    参数:
            StepName:VuGen中树形视图中显示的名称,在自动事务处理中也可以用做事务的名称。

            List of Attributes(服务器端和客户端映射的图片):SRC属性是一定会被录制到的,其他的ALT、Frame、TargetFrame、Ordinal则是有的话会被录制到。

    1、ALT:描述图象的元素。用鼠标指向图象时,所浮出来的文字提示。

    2、SRC:描述图象的元素,可以是图象的文件名. 如: button.gif。也可以使用SRC/SFX来指定图象路径的后缀。所有拥有相同此后缀的字符串都会被匹配到。

    3、Frame:录制操作时所在的Frame的名称。

    4、TargetFrame:见List of Attributes的同名参数。

    5、Ordinal:参见Web_link的同名参数。

            List of Attributes(客户端映射的图片):

    1、AreaAlt:鼠标单击区域的ALT属性。

    2、AreaOrdinal:鼠标单击区域的顺序号。

    3、MapName:图象的映射名。

            List of Attributes(服务器端映射的图片):尽管点击坐标不属于属性,但还是以属性的格式来使用。

    1、Xcoord:点击图象时的X坐标。

    2、Ycoord:点击图象时的Y坐标。

            EXTRARES:分隔符,标记下一个参数是资源属性的列表了。

            List of Resource Attributes:参见List of Resource Attributes一节。

            LAST:属性列表结束的标记符。

    说明
            web_image模拟鼠标在指定图片上的单击动作。此函数必须在有前置操作的上下文中使用。

            在Toos—Recording Option,如果录制级别设为基于HMTL的录制方式时,web_image才会被录制到。

            web_image支持客户端(client-side)和服务器端server-side的图片映射。

            在录制选项中,Toos—Recording Option下,Recording选项中,有一个Advanced HTML选项,可以设置是否录制非HTML资源,只有选择了“Record within the current scrīpt step”时,List of Resource Attributes才会被录制到。非HTML资源的例子是gif和jpg图象文件。

            通过修改HTTP头可以传递给服务器一些请求附加信息。使用HTTP头允许请求中包含内容,如同压缩文件一样。还可以只请求特定状态的web页面。

            web_image支持Web虚拟用户,不支持WAP虚拟用户。

    例子
            下面的例子模拟用户单击Home图标以回到主页(黑体部分):

    web_url("my_home", "URL=http://my_home/", LAST);

    web_link("Employees", "Text=Employees", LAST);

    web_image("Home.gif", "SRC=../gifs/Buttons/Home.gif", LAST);

    web_link("Library", "Text=Library", LAST);

    web_image("Home.gif", "SRC=../../gifs/buttons/Home.gif", LAST);

            下面的例子模拟用户在客户端映射的图片上单击:

    web_image("dpt_house.gif",

           "Src=../gifs/dpt_house.gif",

           "MapName=dpt_house",

           "AreaOrdinal=4",

           LAST);

            下面的例子模拟用户在服务端映射的图片上单击:

    web_image("The Web Developer's Virtual Library",

           "Alt=The Web Developer's Virtual Library",

           "Ordinal=1",

           "XCoord=91",

           "YCoord=17",

           LAST);

            下面是一个使用文件名后缀的例子:它指定了dpt_house.gif作为后缀,所以象../gifs/dpt_house.gif、/gifs/dpt_house.gif、gifs/dpt_house.gif、/dpt_house.gif等都会匹配到。

    web_image("dpt_house.gif",
            "Src/sfx=dpt_house.gif", LAST);

    web_link
    语法:
            Int web_link (const char *StepName, <List of Attributes>, [EXTRARES, <List of Resource Attributes>,] LAST );

    返回值
            成功时返回LR_PASS (0),失败时返回 LR_FAIL (1)。

    参数:
            StepName:VuGen中树形视图中显示的名称,在自动事务设置中也被用做事务名称。

            List of Attributes:支持下列的属性:

    1.      Text:超链接中的文字,必须精确匹配。

    2.      Frame:录制操作时所在的Frame的名称。

    3.      TargetFrame、ResourceByteLimit:见List of Attributes一节。

    4.      Ordinal:如果用给出的属性(Attributes)筛选出的元素不唯一,那么VuGen使用此属性来指定其中的一个。例如:“SRC=abc.gif”,“Ordinal=3”标记的是SRC的值是“abc.gif”的第3张图片。

            EXTRARES:表明下面的参数将会是list of resource attributes了。

            LAST:结尾标示符。

  • QTP关键技术

    2007-09-20 17:13:05

    一 对象识别及存储技术基本常识

    1)测试对象模型(Test Object Model)
            测试对象模型是QTP用来描述应用程序中对象的一组对象类。每个测试对象类拥有一系列用于唯一确定对象属性和一组QTP能够录制的方法
     
    2)测试对象(Test Object)
            用于描述应用程序实际对象的对象,QTP存储这些信息用来在运行时识别和检查对象
     
    3)运行时对象(Run-Time Object)
            是应用程序中的实际对象,对象的方法将在运行中被执行
     
    4)QTP的录制过程
            A.确定用于描述当前操作对象的测试对象类,并创建测试对象
            B.读取当前操作对象属性的当前值,并存储一组属性和属性值到测试对象中
            C.为测试对象创建一个独特的有别于其他对象的名称,通常使用一个突出属性的值
            D.记录在对象上执行的操作
     
    5)QTP的回放过程
            A.根据对象的名称到对象存储库(Object Repository)中查找相应的对象
            B.读取对象的描述,即对象的属性和属性值
            C.基于对象的描述,QTP在应用程序中查找相应的对象
            D.执行相关的操作
     
    二 对Check Point的较为深入理解

    1. 定义:
            将特定属性的当前数据与期望数据进行比较的检查点,用于判定被测试程序功能是否正确
            Check Point可以分两类:QTP内置验证点和自定义验证点
     
    2. QTP内置验证点实现原理及优缺点
            A.录制时,根据用户设置的验证内容,记录数据作为基线数据
            B.回放时,QTP捕获对象运行时的数据,与脚本中的基线数据进行比较
            C.如果基线数据和运行数据相同,结果为PASS,反之为Failed.
            D.优点是 操作简单方便
            E.缺点是 QTP默认的检查的属性有时不符合自己的要求,如希望得到检查的属性没有在里面, 而默认的属性不需要检查等。
     
    3. QTP内置验证点结果的应用
            A.录制的验证点在没有进行调整前,仅仅是给出了检查结果是通过还是错误的
            B.实际的测试过程中,可以根据验证点的结果进行不同的操作
           If Window("Flight Reservation").WinEdit("Name:").Check(CheckPoint("Name:")) = True then
                  msgbox "oh, success!"
    Else
                  msgbox "oh, failure!"
    End If
     
    4. 自定义验证点的应用及优缺点
            A.使用条件语句对实际值和期望值进行对比,然后用Reporter对象报告结果
           '检查Ticket Number
    If CStr(dbTicketNumber) = CStr(DataTable("oTicketNumber", dtLocalSheet)) Then
           Reporter.ReportEvent micPass, "打开订单- TicketNumber", "期望结果是:" & dbTicketNumber & ", 界面显示实际结果是:" & DataTable("oTicketNumber", dtLocalSheet)
    Else
           Reporter.ReportEvent micPass, "打开订单- TicketNumber", "期望结果是:" & dbTicketNumber & ", 界面显示实际结果是:" & DataTable("oTicketNumber", dtLocalSheet)
    End If
            B.优点是 非常灵活,前者实现的所有检查都可以用此方法来实现;
            C.缺点是 代码量大,对测试人员的要求高。
     
    5. 对Check Point的深入理解

            A.个人认为在比较简单的和有Active Screen的情况下可以使用QTP内置的Check Point,在比较复杂的情况下可以通过编程和使用Reporter来完成.
            B.在使用check方法时,必须先在Keyword View或者Active Screen中新建CheckPoint。否则无法对该对象进行check,系统报错说无法在对象仓库中找到此对象。如果插入检查点,系统会自动把相关的对象添加到对象库中。
            我认为检查点并不是一个实实在在的对象。因为你可以对同一个对象设置不同的检查点,可以把它的某个属性既设定成True,也可以设定为False。而对象库中的对象的属性值是必须依赖于对象的实际属性值的。如果随意更改有可能无法识别。还有就是可以针对同一个对象设定多个检查点。在测试窗口中可以看到这两个检查点的名称是区分开来的。所以我认为检查点并不是实际存在的对象,而是一些类似映射的东西。
            尽管检查点并不是对象库中的实在的对象,但是它必须对应到对象库中的某个实实在在的对象,好像它的一个映像一样,而且在实际的操作过程中,QTP还是把它作为一个对象来处理的。
            因为我们无法像其他对象一样把“检查点对象”添加到对象库中,而QTP又认为它是个对象,所以我们无法在专家视图中直接添加检查点脚本。但是我们可以采用编成描述的方式来实现检查点的功能。
            CheckPoint 是一个依赖于Object Repository(对象库)中的某个对象的“虚拟对象”。其具体含义是:如果它所依赖的QTP 对象库中的对象没有了,那么此CheckPoint 也就不存在了;这个“虚拟对象”的属性是从它所依赖的对象的属性中“抽取”出来的,它具有它所依赖的对象的一个或几个属性,但不能增加它所依赖的对象没有的任何属性。
            CheckPoint 是一个“虚拟对象”的重要原因是:每个Object都能在Object Repository找到它的Name、Class Properties,而CheckPoint 在Object Repository中就根本不存在。选择脚本中的某个对象后,在Object Property 的对话框里面有个Respository按钮,点击它后,你会看到此对象在Object Respository 的Name、Class 和 Properties。
            选择一个CheckPoint后,在CheckPoint Properties 的对话框里没有 Respository 按钮,在Object Respository中也找不到此CheckPoint的Name、Class 和 Properties(因为它在对象库中根本就不存在!)。

  • QTP自动化测试流程

    2007-09-20 17:12:17

    QTP自动化测试流程

    1)准备TestCase
            - 在进行自动化之前,将测试内容进行文档化,不建议直接录制脚本
            - 在录制脚本之前设计好脚本,便于录制过程的流畅
            - 由于测试用例设计和脚本开发可能不是同一个人完成,便于团队合作
            - 便于后期的维护
            - 文档化的方式:TD或者文档
     
    2)配置QTP
            QTP支持不同的开发环境,在正式录制之前,需要根据被测程序的开发环境,选择合适的Add-In,并进行加载。
     
    3)录制脚本
            启动QTP的录制功能,按照Test Case的操作步骤描述执行,QTP自动记录每一步操作,并自动生成VBscrīpt脚本。
     
    4)修改增强脚本
            刚刚录制好的脚本可能包含错误,或者没有达到预期的目的,这就需要在录制脚本的基础上,进行修改增强
            - 删除录制过程中多余的以及错误的操作,以最少的脚本完成任务
            - 如果前面操作的输出是后面操作的输入,则需要使用变量或者输出值来进行替换
            - 不是所有的操作都可以通过录制产生的,有些需要通过手工编码实现这些功能
            - 录制产生的脚本是线性的,可以加入条件、循环控制语句,实现更复杂的流程
            - 对脚本进行结构化
            - 加入注释,便于阅读和维护
     
    5)调试脚本
            - 回放通过的脚本,不一定是正确的,也可能会包含错误
            - 在测试脚本正式使用之前,要保证其本身的正确性
            - 避免测试脚本故障和被测程序故障搅在一起,不容易定位
     
    6)回放脚本
            - 对于回放的错误,不要急于马上提交Bug,首先要判断是脚本本身的错误还是程序的错误,确认后再提交。
     
    7)脚本维护
            - 随着工作的不断推进,脚本量会越来越多
            - 被测试程序的不断更新,也需要更新相应的测试脚本
            - 采用版本管理工具保存脚本,如CVS、VSS,可以随时获取历史版本
            - 采用统一的脚本架构
            - 采用统一的命名规范
            - 添加充分的注释,避免时间久了,自己都不能马上读懂脚本

  • 全面介绍单元测试 -转贴

    2007-09-16 13:53:42

    这是一篇全面介绍单元测试的经典之作,对理解单元测试和Visual Unit很有帮助,作者老纳,收录时作了少量修改]

    一 单元测试概述
      工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。
      其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么 的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未 覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。 可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
      对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
      要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。
      一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位, 复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因 为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
      有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包 含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在 头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。
      什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过 分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产 品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后 再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
      由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。
      关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩 函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减 少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。

    二 测试代码编写
      多数讲述单元测试的文章都是以Java为例,本文以C++为例,后半部分所介绍的单元测试工具也只介绍C++单元测试工具。下面的示例代码的开发环境是VC6.0。

    产品类:
    class CMyClass
    {
    public:
        int Add(int i, int j);
        CMyClass();
        virtual ~CMyClass();

    private:
        int mAge;      //年龄
        CString mPhase; //年龄阶段,如"少年","青年"
    };

    建立对应的测试类CMyClassTester,为了节约编幅,只列出源文件的代码:
    void CMyClassTester::CaseBegin()
    {
        //pObj是CMyClassTester类的成员变量,是被测试类的对象的指针,
        //为求简单,所有的测试类都可以用pObj命名被测试对象的指针。
        pObj = new CMyClass();
    }

    void CMyClassTester::CaseEnd()
    {
        delete pObj;
    }
    测试类的函数CaseBegin()和CaseEnd()建立和销毁被测试对象,每个测试用例的开头都要调用CaseBegin(),结尾都要调用CaseEnd()。

    接下来,我们建立示例的产品函数:
    int CMyClass::Add(int i, int j)
    {
        return i+j;
    }
    和对应的测试函数:
    void CMyClassTester::Add_int_int()
    {
    }
    把参数表作为函数名的一部分,这样当出现重载的被测试函数时,测试函数不会产生命名冲突。下面添加测试用例:
    void CMyClassTester::Add_int_int()
    {
        //第一个测试用例
        CaseBegin();{              //1
        int i = 0;                //2
        int j = 0;                //3
        int ret = pObj->Add(i, j); //4
        ASSERT(ret == 0);          //5
        }CaseEnd();                //6
    }
    第1和第6行建立和销毁被测试对象,所加的{}是为了让每个测试用例的代码有一个独立的域,以便多个测试用例使用相同的变量名。
    第2和第3行是定义输入数据,第4行是调用被测试函数,这些容易理解,不作进一步解释。第5行是预期输出,它的特点是当实际输出与预期输出不同时自动报 错,ASSERT是VC的断言宏,也可以使用其他类似功能的宏,使用测试工具进行单元测试时,可以使用该工具定义的断言宏。

      示例中的格式显得很不简洁,2、3、4、5行可以合写为一行:ASSERT(pObj->Add(0, 0) == 0);但这种不简洁的格式却是老纳极力推荐的,因为它一目了然,易于建立多个测试用例,并且具有很好的适应性,同时,也是极佳的代码文档,总之,老纳建 议:输入数据和预期输出要自成一块。
      建立了第一个测试用例后,应编译并运行测试,以排除语法错误,然后,使用拷贝/修改的办法建立其他测试用例。由于各个测试用例之间的差别往往很小,通常只需修改一两个数据,拷贝/修改是建立多个测试用例的最快捷办法。

    三 测试用例
      下面说说测试用例、输入数据及预期输出。输入数据是测试用例的核心,老纳对输入数据的定义是:被测试函数所读取的外部数据及这些数据的初始值。外部数 据是对于被测试函数来说的,实际上就是除了局部变量以外的其他数据,老纳把这些数据分为几类:参数、成员变量、全局变量、IO媒体。IO媒体是指文件、数 据库或其他储存或传输数据的媒体,例如,被测试函数要从文件或数据库读取数据,那么,文件或数据库中的原始数据也属于输入数据。一个函数无论多复杂,都无 非是对这几类数据的读取、计算和写入。预期输出是指:返回值及被测试函数所写入的外部数据的结果值。返回值就不用说了,被测试函数进行了写操作的参数(输 出参数)、成员变量、全局变量、IO媒体,它们的预期的结果值都是预期输出。一个测试用例,就是设定输入数据,运行被测试函数,然后判断实际输出是否符合 预期。下面举一个与成员变量有关的例子:
    产品函数:
    void CMyClass::Grow(int years)
    {
        mAge += years;

        if(mAge < 10)
            mPhase = "儿童";
        else if(mAge <20)
            mPhase = "少年";
        else if(mAge <45)
            mPhase = "青年";
        else if(mAge <60)
            mPhase = "中年";
        else
            mPhase = "老年";
    }

    测试函数中的一个测试用例:
        CaseBegin();{
        int years = 1;
        pObj->mAge = 8;
        pObj->Grow(years);
        ASSERT( pObj->mAge == 9 );
        ASSERT( pObj->mPhase == "儿童" );
        }CaseEnd();
    在输入数据中对被测试类的成员变量mAge进行赋值,在预期输出中断言成员变量的值。现在可以看到老纳所推荐的格式的好处了吧,这种格式可以适应很复杂的 测试。在输入数据部分还可以调用其他成员函数,例如:执行被测试函数前可能需要读取文件中的数据保存到成员变量,或需要连接数据库,老纳把这些操作称为初 始化操作。例如,上例中 ASSERT( ...)之前可以加pObj->OpenFile();。为了访问私有成员,可以将测试类定义为产品类的友元类。例如,定义一个宏:
    #define UNIT_TEST(cls) friend class cls##Tester;
    然后在产品类声明中加一行代码:UNIT_TEST(ClassName)。

      下面谈谈测试用例设计。前面已经说了,测试用例的核心是输入数据。预期输出是依据输入数据和程序功能来确定的,也就是说,对于某一程序,输入数据确定 了,预期输出也就可以确定了,至于生成/销毁被测试对象和运行测试的语句,是所有测试用例都大同小异的,因此,我们讨论测试用例时,只讨论输入数据。
      前面说过,输入数据包括四类:参数、成员变量、全局变量、IO媒体,这四类数据中,只要所测试的程序需要执行读操作的,就要设定其初始值,其中,前两 类比较常用,后两类较少用。显然,把输入数据的所有可能取值都进行测试,是不可能也是无意义的,我们应该用一定的规则选择有代表性的数据作为输入数据,主 要有三种:正常输入,边界输入,非法输入,每种输入还可以分类,也就是平常说的等价类法,每类取一个数据作为输入数据,如果测试通过,可以肯定同类的其他 输入也是可以通过的。下面举例说明:
      正常输入
      例如字符串的Trim函数,功能是将字符串前后的空格去除,那么正常的输入可以有四类:前面有空格;后面有空格;前后均有空格;前后均无空格。
      边界输入
      上例中空字符串可以看作是边界输入。
      再如一个表示年龄的参数,它的有效范围是0-100,那么边界输入有两个:0和100。
      非法输入
      非法输入是正常取值范围以外的数据,或使代码不能完成正常功能的输入,如上例中表示年龄的参数,小于0或大于100都是非法输入,再如一个进行文件操作的函数,非法输入有这么几类:文件不存在;目录不存在;文件正在被其他程序打开;权限错误。
      如果函数使用了外部数据,则正常输入是肯定会有的,而边界输入和非法输入不是所有函数都有。一般情况下,即使没有设计文档,考虑以上三种输入也可以找 出函数的基本功能点。实际上,单元测试与代码编写是“一体两面”的关系,编码时对上述三种输入都是必须考虑的,否则代码的健壮性就会成问题。

    四 白盒覆盖
      上面所说的测试数据都是针对程序的功能来设计的,就是所谓的黑盒测试。单元测试还需要从另一个角度来设计测试数据,即针对程序的逻辑结构来设计测试用 例,就是所谓的白盒测试。在老纳看来,如果黑盒测试是足够充分的,那么白盒测试就没有必要,可惜“足够充分”只是一种理想状态,例如:真的是所有功能点都 测试了吗?程序的功能点是人为的定义,常常是不全面的;各个输入数据之间,有些组合可能会产生问题,怎样保证这些组合都经过了测试?难于衡量测试的完整性 是黑盒测试的主要缺陷,而白盒测试恰恰具有易于衡量测试完整性的优点,两者之间具有极好的互补性,例如:完成功能测试后统计语句覆盖率,如果语句覆盖未完 成,很可能是未覆盖的语句所对应的功能点未测试。
      白盒测试针对程序的逻辑结构设计测试用例,用逻辑覆盖率来衡量测试的完整性。逻辑单位主要有:语句、分支、条件、条件值、条件值组合,路径。语句覆盖 就是覆盖所有的语句,其他类推。另外还有一种判定条件覆盖,其实是分支覆盖与条件覆盖的组合,在此不作讨论。跟条件有关的覆盖就有三种,解释一下:条件覆 盖是指覆盖所有的条件表达式,即所有的条件表达式都至少计算一次,不考虑计算结果;条件值覆盖是指覆盖条件的所有可能取值,即每个条件的取真值和取假值都 要至少计算一次;条件值组合覆盖是指覆盖所有条件取值的所有可能组合。老纳做过一些粗浅的研究,发现与条件直接有关的错误主要是逻辑操作符错误,例如: ||写成&&,漏了写!什么的,采用分支覆盖与条件覆盖的组合,基本上可以发现这些错误,另一方面,条件值覆盖与条件值组合覆盖往往需要 大量的测试用例,因此,在老纳看来,条件值覆盖和条件值组合覆盖的效费比偏低。老纳认为效费比较高且完整性也足够的测试要求是这样的:完成功能测试,完成 语句覆盖、条件覆盖、分支覆盖、路径覆盖。做过单元测试的朋友恐怕会对老纳提出的测试要求给予一个字的评价:晕!或者两个字的评价:狂晕!因为这似乎是不 可能的要求,要达到这种测试完整性,其测试成本是不可想象的,不过,出家人不打逛语,老纳之所以提出这种测试要求,是因为利用一些工具,可以在较低的成本 下达到这种测试要求,后面将会作进一步介绍。
      关于白盒测试用例的设计,程序测试领域的书籍一般都有讲述,普通方法是画出程序的逻辑结构图如程序流程图或控制流图,根据逻辑结构图设计测试用例,这 些是纯粹的白盒测试,不是老纳想推荐的方式。老纳所推荐的方法是:先完成黑盒测试,然后统计白盒覆盖率,针对未覆盖的逻辑单位设计测试用例覆盖它,例如, 先检查是否有语句未覆盖,有的话设计测试用例覆盖它,然后用同样方法完成条件覆盖、分支覆盖和路径覆盖,这样的话,既检验了黑盒测试的完整性,又避免了重 复的工作,用较少的时间成本达到非常高的测试完整性。不过,这些工作可不是手工能完成的,必须借助于工具,后面会介绍可以完成这些工作的测试工具。

    五 单元测试工具
      现在开始介绍单元测试工具,老纳只介绍三种,都是用于C++语言的。
      首先是CppUnit,这是C++单元测试工具的鼻祖,免费的开源的单元测试框架。由于已有一众高人写了不少关于CppUnit的很好的文章,老纳就不现丑了,想了解CppUnit的朋友,建议读一下Cpluser 所作的《CppUnit测试框架入门》,网址是:http://blog.csdn.net/cpluser/archive/2004/09/21/111522.aspx。该文也提供了CppUnit的下载地址。
      然后介绍C++Test,这是Parasoft公司的产品。[C++Test是一个功能强大的自动化C/C++单元级测试工具,可以自动测试任何 C/C++函数、类,自动生成测试用例、测试驱动函数或桩函数,在自动化的环境下极其容易快速的将单元级的测试覆盖率达到100%]。[]内的文字引自http://www.superst.com.cn/softwares_testing_c_cpptest.htm, 这是华唐公司的网页。老纳想写些介绍C++Test的文字,但发现无法超越华唐公司的网页上的介绍,所以也就省点事了,想了解C++Test的朋友,建议 访问该公司的网站。华唐公司代理C++Test,想要购买或索取报价、试用版都可以找他们。老纳帮华唐公司做广告,不知道会不会得点什么好处?
      最后介绍Visual Unit,简称VU,这是国产的单元测试工具,据说申请了多项专利,拥有一批创新的技术,不过老纳只关心是不是有用和好用。[自动生成测试代码 快速建立功能测试用例 程序行为一目了然 极高的测试完整性 高效完成白盒覆盖 快速排错 高效调试 详尽的测试报告]。[]内的文字是VU开发商的网页上摘录的,网址是:http://www.unitware.cn。 前面所述测试要求:完成功能测试,完成语句覆盖、条件覆盖、分支覆盖、路径覆盖,用VU可以轻松实现,还有一点值得一提:使用VU还能提高编码的效率,总 体来说,在完成单元测试的同时,编码调试的时间还能大幅度缩短。算了,不想再讲了,老纳显摆理论、介绍经验还是有兴趣的,因为可以满足老纳好为人师的虚荣 心,但介绍工具就觉得索然无味了,毕竟工具好不好用,合不合用,要试过才知道,还是自己去开发商的网站看吧,可以下载演示版,还有演示课件。
    这是一篇全面介绍单元测试的经典之作,对理解单元测试和Visual Unit很有帮助,作者老纳,收录时作了少量修改]

    一 单元测试概述
      工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。
      其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么 的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未 覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。 可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
      对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
      要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。
      一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位, 复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因 为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
      有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包 含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在 头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。
      什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过 分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产 品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后 再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
      由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。
      关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩 函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减 少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。

    二 测试代码编写
      多数讲述单元测试的文章都是以Java为例,本文以C++为例,后半部分所介绍的单元测试工具也只介绍C++单元测试工具。下面的示例代码的开发环境是VC6.0。

    产品类:
    class CMyClass
    {
    public:
        int Add(int i, int j);
        CMyClass();
        virtual ~CMyClass();

    private:
        int mAge;      //年龄
        CString mPhase; //年龄阶段,如"少年","青年"
    };

    建立对应的测试类CMyClassTester,为了节约编幅,只列出源文件的代码:
    void CMyClassTester::CaseBegin()
    {
        //pObj是CMyClassTester类的成员变量,是被测试类的对象的指针,
        //为求简单,所有的测试类都可以用pObj命名被测试对象的指针。
        pObj = new CMyClass();
    }

    void CMyClassTester::CaseEnd()
    {
        delete pObj;
    }
    测试类的函数CaseBegin()和CaseEnd()建立和销毁被测试对象,每个测试用例的开头都要调用CaseBegin(),结尾都要调用CaseEnd()。

    接下来,我们建立示例的产品函数:
    int CMyClass::Add(int i, int j)
    {
        return i+j;
    }
    和对应的测试函数:
    void CMyClassTester::Add_int_int()
    {
    }
    把参数表作为函数名的一部分,这样当出现重载的被测试函数时,测试函数不会产生命名冲突。下面添加测试用例:
    void CMyClassTester::Add_int_int()
    {
        //第一个测试用例
        CaseBegin();{              //1
        int i = 0;                //2
        int j = 0;                //3
        int ret = pObj->Add(i, j); //4
        ASSERT(ret == 0);          //5
        }CaseEnd();                //6
    }
    第1和第6行建立和销毁被测试对象,所加的{}是为了让每个测试用例的代码有一个独立的域,以便多个测试用例使用相同的变量名。
    第2和第3行是定义输入数据,第4行是调用被测试函数,这些容易理解,不作进一步解释。第5行是预期输出,它的特点是当实际输出与预期输出不同时自动报 错,ASSERT是VC的断言宏,也可以使用其他类似功能的宏,使用测试工具进行单元测试时,可以使用该工具定义的断言宏。

      示例中的格式显得很不简洁,2、3、4、5行可以合写为一行:ASSERT(pObj->Add(0, 0) == 0);但这种不简洁的格式却是老纳极力推荐的,因为它一目了然,易于建立多个测试用例,并且具有很好的适应性,同时,也是极佳的代码文档,总之,老纳建 议:输入数据和预期输出要自成一块。
      建立了第一个测试用例后,应编译并运行测试,以排除语法错误,然后,使用拷贝/修改的办法建立其他测试用例。由于各个测试用例之间的差别往往很小,通常只需修改一两个数据,拷贝/修改是建立多个测试用例的最快捷办法。

    三 测试用例
      下面说说测试用例、输入数据及预期输出。输入数据是测试用例的核心,老纳对输入数据的定义是:被测试函数所读取的外部数据及这些数据的初始值。外部数 据是对于被测试函数来说的,实际上就是除了局部变量以外的其他数据,老纳把这些数据分为几类:参数、成员变量、全局变量、IO媒体。IO媒体是指文件、数 据库或其他储存或传输数据的媒体,例如,被测试函数要从文件或数据库读取数据,那么,文件或数据库中的原始数据也属于输入数据。一个函数无论多复杂,都无 非是对这几类数据的读取、计算和写入。预期输出是指:返回值及被测试函数所写入的外部数据的结果值。返回值就不用说了,被测试函数进行了写操作的参数(输 出参数)、成员变量、全局变量、IO媒体,它们的预期的结果值都是预期输出。一个测试用例,就是设定输入数据,运行被测试函数,然后判断实际输出是否符合 预期。下面举一个与成员变量有关的例子:
    产品函数:
    void CMyClass::Grow(int years)
    {
        mAge += years;

        if(mAge < 10)
            mPhase = "儿童";
        else if(mAge <20)
            mPhase = "少年";
        else if(mAge <45)
            mPhase = "青年";
        else if(mAge <60)
            mPhase = "中年";
        else
            mPhase = "老年";
    }

    测试函数中的一个测试用例:
        CaseBegin();{
        int years = 1;
        pObj->mAge = 8;
        pObj->Grow(years);
        ASSERT( pObj->mAge == 9 );
        ASSERT( pObj->mPhase == "儿童" );
        }CaseEnd();
    在输入数据中对被测试类的成员变量mAge进行赋值,在预期输出中断言成员变量的值。现在可以看到老纳所推荐的格式的好处了吧,这种格式可以适应很复杂的 测试。在输入数据部分还可以调用其他成员函数,例如:执行被测试函数前可能需要读取文件中的数据保存到成员变量,或需要连接数据库,老纳把这些操作称为初 始化操作。例如,上例中 ASSERT( ...)之前可以加pObj->OpenFile();。为了访问私有成员,可以将测试类定义为产品类的友元类。例如,定义一个宏:
    #define UNIT_TEST(cls) friend class cls##Tester;
    然后在产品类声明中加一行代码:UNIT_TEST(ClassName)。

      下面谈谈测试用例设计。前面已经说了,测试用例的核心是输入数据。预期输出是依据输入数据和程序功能来确定的,也就是说,对于某一程序,输入数据确定 了,预期输出也就可以确定了,至于生成/销毁被测试对象和运行测试的语句,是所有测试用例都大同小异的,因此,我们讨论测试用例时,只讨论输入数据。
      前面说过,输入数据包括四类:参数、成员变量、全局变量、IO媒体,这四类数据中,只要所测试的程序需要执行读操作的,就要设定其初始值,其中,前两 类比较常用,后两类较少用。显然,把输入数据的所有可能取值都进行测试,是不可能也是无意义的,我们应该用一定的规则选择有代表性的数据作为输入数据,主 要有三种:正常输入,边界输入,非法输入,每种输入还可以分类,也就是平常说的等价类法,每类取一个数据作为输入数据,如果测试通过,可以肯定同类的其他 输入也是可以通过的。下面举例说明:
      正常输入
      例如字符串的Trim函数,功能是将字符串前后的空格去除,那么正常的输入可以有四类:前面有空格;后面有空格;前后均有空格;前后均无空格。
      边界输入
      上例中空字符串可以看作是边界输入。
      再如一个表示年龄的参数,它的有效范围是0-100,那么边界输入有两个:0和100。
      非法输入
      非法输入是正常取值范围以外的数据,或使代码不能完成正常功能的输入,如上例中表示年龄的参数,小于0或大于100都是非法输入,再如一个进行文件操作的函数,非法输入有这么几类:文件不存在;目录不存在;文件正在被其他程序打开;权限错误。
      如果函数使用了外部数据,则正常输入是肯定会有的,而边界输入和非法输入不是所有函数都有。一般情况下,即使没有设计文档,考虑以上三种输入也可以找 出函数的基本功能点。实际上,单元测试与代码编写是“一体两面”的关系,编码时对上述三种输入都是必须考虑的,否则代码的健壮性就会成问题。

    四 白盒覆盖
      上面所说的测试数据都是针对程序的功能来设计的,就是所谓的黑盒测试。单元测试还需要从另一个角度来设计测试数据,即针对程序的逻辑结构来设计测试用 例,就是所谓的白盒测试。在老纳看来,如果黑盒测试是足够充分的,那么白盒测试就没有必要,可惜“足够充分”只是一种理想状态,例如:真的是所有功能点都 测试了吗?程序的功能点是人为的定义,常常是不全面的;各个输入数据之间,有些组合可能会产生问题,怎样保证这些组合都经过了测试?难于衡量测试的完整性 是黑盒测试的主要缺陷,而白盒测试恰恰具有易于衡量测试完整性的优点,两者之间具有极好的互补性,例如:完成功能测试后统计语句覆盖率,如果语句覆盖未完 成,很可能是未覆盖的语句所对应的功能点未测试。
      白盒测试针对程序的逻辑结构设计测试用例,用逻辑覆盖率来衡量测试的完整性。逻辑单位主要有:语句、分支、条件、条件值、条件值组合,路径。语句覆盖 就是覆盖所有的语句,其他类推。另外还有一种判定条件覆盖,其实是分支覆盖与条件覆盖的组合,在此不作讨论。跟条件有关的覆盖就有三种,解释一下:条件覆 盖是指覆盖所有的条件表达式,即所有的条件表达式都至少计算一次,不考虑计算结果;条件值覆盖是指覆盖条件的所有可能取值,即每个条件的取真值和取假值都 要至少计算一次;条件值组合覆盖是指覆盖所有条件取值的所有可能组合。老纳做过一些粗浅的研究,发现与条件直接有关的错误主要是逻辑操作符错误,例如: ||写成&&,漏了写!什么的,采用分支覆盖与条件覆盖的组合,基本上可以发现这些错误,另一方面,条件值覆盖与条件值组合覆盖往往需要 大量的测试用例,因此,在老纳看来,条件值覆盖和条件值组合覆盖的效费比偏低。老纳认为效费比较高且完整性也足够的测试要求是这样的:完成功能测试,完成 语句覆盖、条件覆盖、分支覆盖、路径覆盖。做过单元测试的朋友恐怕会对老纳提出的测试要求给予一个字的评价:晕!或者两个字的评价:狂晕!因为这似乎是不 可能的要求,要达到这种测试完整性,其测试成本是不可想象的,不过,出家人不打逛语,老纳之所以提出这种测试要求,是因为利用一些工具,可以在较低的成本 下达到这种测试要求,后面将会作进一步介绍。
      关于白盒测试用例的设计,程序测试领域的书籍一般都有讲述,普通方法是画出程序的逻辑结构图如程序流程图或控制流图,根据逻辑结构图设计测试用例,这 些是纯粹的白盒测试,不是老纳想推荐的方式。老纳所推荐的方法是:先完成黑盒测试,然后统计白盒覆盖率,针对未覆盖的逻辑单位设计测试用例覆盖它,例如, 先检查是否有语句未覆盖,有的话设计测试用例覆盖它,然后用同样方法完成条件覆盖、分支覆盖和路径覆盖,这样的话,既检验了黑盒测试的完整性,又避免了重 复的工作,用较少的时间成本达到非常高的测试完整性。不过,这些工作可不是手工能完成的,必须借助于工具,后面会介绍可以完成这些工作的测试工具。

    五 单元测试工具
      现在开始介绍单元测试工具,老纳只介绍三种,都是用于C++语言的。
      首先是CppUnit,这是C++单元测试工具的鼻祖,免费的开源的单元测试框架。由于已有一众高人写了不少关于CppUnit的很好的文章,老纳就不现丑了,想了解CppUnit的朋友,建议读一下Cpluser 所作的《CppUnit测试框架入门》,网址是:http://blog.csdn.net/cpluser/archive/2004/09/21/111522.aspx。该文也提供了CppUnit的下载地址。
      然后介绍C++Test,这是Parasoft公司的产品。[C++Test是一个功能强大的自动化C/C++单元级测试工具,可以自动测试任何 C/C++函数、类,自动生成测试用例、测试驱动函数或桩函数,在自动化的环境下极其容易快速的将单元级的测试覆盖率达到100%]。[]内的文字引自http://www.superst.com.cn/softwares_testing_c_cpptest.htm, 这是华唐公司的网页。老纳想写些介绍C++Test的文字,但发现无法超越华唐公司的网页上的介绍,所以也就省点事了,想了解C++Test的朋友,建议 访问该公司的网站。华唐公司代理C++Test,想要购买或索取报价、试用版都可以找他们。老纳帮华唐公司做广告,不知道会不会得点什么好处?
      最后介绍Visual Unit,简称VU,这是国产的单元测试工具,据说申请了多项专利,拥有一批创新的技术,不过老纳只关心是不是有用和好用。[自动生成测试代码 快速建立功能测试用例 程序行为一目了然 极高的测试完整性 高效完成白盒覆盖 快速排错 高效调试 详尽的测试报告]。[]内的文字是VU开发商的网页上摘录的,网址是:http://www.unitware.cn。 前面所述测试要求:完成功能测试,完成语句覆盖、条件覆盖、分支覆盖、路径覆盖,用VU可以轻松实现,还有一点值得一提:使用VU还能提高编码的效率,总 体来说,在完成单元测试的同时,编码调试的时间还能大幅度缩短。算了,不想再讲了,老纳显摆理论、介绍经验还是有兴趣的,因为可以满足老纳好为人师的虚荣 心,但介绍工具就觉得索然无味了,毕竟工具好不好用,合不合用,要试过才知道,还是自己去开发商的网站看吧,可以下载演示版,还有演示课件。




    文中提到的Visual Unit,现在有免费版本了
    Visual Unit 简介
    Visual Unit(VU) 解决了实施单元测试面临的主要问题:单元测试降低编码阶段的生产效率? VU自动生成测试代码,全方位示出程序行为,帮助整理和验证编码思路,支持快速排错和高效调试,边编码边测试反而可以提高编码的生产率;开发人员不喜欢测 试自已编写的代码? VU使程序的功能和逻辑结构一目了然,既是测试工具,也是编码辅助工具,提高了编程的舒适度,容易让开发人员接受和喜爱;单元测试的效果难于保证、难于持 续实施,并行开发难于保证覆盖率? VU可轻松完成100%语句、条件、分支、路径覆盖,提供详尽的测试报告和待测试文件列表,随时可以检验测试效果、找出遗漏代码或未完成覆盖的代码,保证 测试的完整性,易于持续实施。
    Visual Unit目前的版本支持VC6.0,VC.Net,C++Builder 6.0。
    Visual Unit的发布版本包括企业版和个人版,其中,个人版是完全免费的版本

    下载安装
    可从官方网站下载Visual Unit 1.1,网址为http://www.UnitWare.cn。安装包只有5.67M,但已包含了个人版和企业版。安装后,个人版即可免费使用,企业版在经过简单的注册后,可以免费试用一段时间。

    开始使用Visual Unit
    下面是VU的入门操作,根据帮助系统中的《VU入门指引》修改而成,实际使用时建议直接阅读该指引,VU第一次启动时会询问是否浏览该指引。

    1. 打开示例工程或新建测试工程
    打开示例工程:
    启动您的开发环境(如VC6.0),打开示例的测试工程,目录为:@ROOT@\Samples\@IDE@\TestDemo\
    将以下目录添加到开发环境的搜索路径:@ROOT@\include\ 及 @ROOT@\Samples\@IDE@\Demo\
    @ROOT@表示VU的安装目录,如C:\Program Files\Visual Unit。
    @IDE@表示开发环境的名称,目前有四种:VC 6.0、VC .Net、VC.Net 2003、C++ Builder。
    测试用例编辑器中可以阅读每一个示例的说明,该说明位于测试用例摘要下方。初学者最好看一下帮助系统中《关于示例的说明》。

    或新建测试工程:
    不同的开发环境建立和配置测试工程的操作稍有不同,请按照帮助系统的说明进行。
    如果IDE是C++ Builder,测试时要在IDE中打开测试工程根目录下的VuxCodeImp.h文件,请阅读帮助系统《关于C++Builder的特殊事项》。

    2. 选择要测试的产品文件和要测试的函数,自动生成测试文件和测试函数
    在导航窗口中选择一个产品文件,如果测试文件不存在会弹出提示,生成测试文件;
    在导航窗口的函数列表中选择一个函数,如果测试函数不存在会弹出提示,生成测试函数,并自动弹出测试用例编辑器。

    3. 填写测试用例
    在测试用例编辑器中“输入数据”和“预期输出”输入框中填写测试用例的输入和预期的输出。点击“新建”按钮将复制当前测试用例,修改输入和输出即可获得新的测试用例。

    4. 运行测试
    用您的开发环境编译并运行测试工程,即可执行测试。测试完毕,主窗口自动弹出,显示测试结果。

    示例工程的主要文件是由VC6.0开发的,其他IDE在编译时会产生一些编译警告,可以忽略这些编译警告,有些代码会产生异常,缺省设置是不作处理,可以 设为捕获异常(导航窗口菜单->选项->扩展,在“捕获异常”复选框前打勾),对于企业版,建议不要捕获异常,程序崩溃时不要即时调试(出现 崩溃窗口时应选择“确定”),观察数据窗口和代码窗口通常可以快速地发现出错位置和出错原因。

    5. 使用IDE插件
    目前版本已经开发了VC6.0插件,使用该插件,点击一个按钮,即可完成步骤2. 3. 4的操作。该插件的安装和使用请查阅帮助系统。[个人版不支持IDE插件]

  • 单元测试------理论篇

    2007-09-16 13:51:40

    单元测试------理论篇(转)
    测试是软件开发的重要环节之一。按照软件开发的过程测试可分为:单元测试、集成测试、系统测试、域测试(Field test)等。我们这里将讨论面向程序员的单元测试。本文首先介绍单元测试的定义,为什么要使用单元测试?单元测试能给我们带来的好处。之后我们将介绍单 元测试的范畴,最后将讨论很多朋友不写单元测试的借口。希望本文能够再次引起您对单元测试的重视,并说服您老板对编写单元测试的支持,能让美丽的单元测试 真正应用到您的项目之中。

    什么是单元测试
          单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件(或者场景) 下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。
        单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。

    为什么要使用单元测试
        我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。
        幸运,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。

    单元测试有下面的这些优点:
    1、它是一种验证行为。
        程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。
    2、它是一种设计行为。
        编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
    3、它是一种编写文档的行为。
        单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
    4、它具有回归性。
        自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。

    单元测试的范畴
        如果要给单元测试定义一个明确的范畴,指出哪些功能是属于单元测试,这似乎很难。但下面讨论的四个问题,基本上可以说明单元测试的范畴,单元测试所要做的工作。
    1、 它的行为和我期望的一致吗?
        这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的。
    2、 它的行为一直和我期望的一致吗?
        编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一个项复杂的工程,在测试某段代码的行为是否和你的期望一 致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。
    3、 我可以依赖单元测试吗?
       不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。
    4、 单元测试说明我的意图了吗?
        单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。

    不写测试的借口
        到这里,我们已经列举了使用单元测试的种种理由。也许,每个人都同意,是的,该做更多的测试。这种人人同意的事情还多着呢,是的,该多吃蔬菜,该戒烟,该多休息,该多锻炼……这并不意味着我们中的所有人都会这么去做,不是吗?
    1、 编写单元测试太花时间了。
        我们知道,在开发时越早发现BUG,就能节省更多的时间,降低更多的风险。
        下图表摘自<<实用软件度量>>(Capers Jones,McGraw-Hill 1991),它列出了准备测试,执行测试,和修改缺陷所花费的时间(以一个功能点为基准),这些数据显示单元测试的成本效率大约是集成测试的两倍,是系统 测试的三倍(参见条形图)。

        术语:域测试(Field test)意思是在软件投入使用以后,针对某个领域所作的所有测试活动。
        如果你仍然认为在编写产品代码的时候,还是没有时间编写测试代码,那么请先考虑下面这些问题:
            1)、对于所编写的代码,你在调试上面花了多少时间。
            2)、对于以前你自认为正确的代码,而实际上这些代码却存在重大的bug,你花了多少时间在重新确认这些代码上面。
            3)、对于一个别人报告的bug,你花了多少时间才找出导致这个bug 的源码位置。
        回答完这些问题,你一定不再以“太花时间”作为拒绝单元测试的借口。
    2、 运行测试的时间太长了。
        合适的测试是不会让这种情况发生的。实际上,大多数测试的执行都是非常快的,因此你在几秒之内就可以运行成千上万个测试。但是有时某些测试会花费很长的时间。这时,需要把这些耗时的测试和其他测试分开。通常可以每天运行这种测试一次,或者几天一次。
    3、 测试代码并不是我的工作。
        你的工作就是保证代码能够正确的完成你的行为,恰恰相反,测试代码正是你不可缺少的工作。
    4、 我并不清楚代码的行为,所以也就无从测试。
       如果你实在不清楚代码的行为,那么估计现在并不是编码的时候。如果你并不知道代码的行为,那么你又如何知道你编写的代码是正确的呢?
    5、 但是这些代码都能够编译通过。
        我们前面已经说过,代码通过编译只是验证它的语法通过。但并不能保证它的行为就一定正确。
    6、 公司请我来是为了写代码,而不是写测试。
        公司付给你薪水是为了让你编写产品代码,而单元测试大体上是一个工具,是一个和编辑器、开发环境、编译器等处于同一位置的工具。
    7、 如果我让测试员或者QA(Quality Assurance)人员没有工作,那么我会觉得很内疚。
       你并不需要担心这些。请记住,我们在此只是谈论单元测试,而它只是一种针对源码的、低层次的,为程序员而设计的测试。在整个项目中,还有其他的很多测试需要这些人来完成,如:功能测试、验收测试、性能测试、环境测试、有效性测试、正确性测试、正规分析等等。
    8、 我的公司并不会让我在真实系统中运行单元测试。
        我们所讨论的只是针对开发者的单元测试。也就是说,如果你可以在其他的环境下(例如在正式的产品系统中)运行这些测试的话,那么它们就不再是单元测试,而是其他类型的测试了。实际上,你可以在你的本机运行单元测试,使用你自己的数据库,或者使用mock 对象。

    总结
      总而言之,单元测试会让我们的开发工作变得更加轻松,让我们对自己的代码更加自信。无论是大型项目还是小型项目,无论是时间紧迫的项目还是时间宽裕的项目,只要代码不是一次写完永不改动,编写单元测试就一定超值,它已成为我们编码不可缺少的一部分。
  • Java程序的单元测试

    2007-09-16 13:50:23

    Java程序的单元测试[分享]
    [本文原发测试时代,于此再发,望有利于各位]
    1.        单元测试的目的
    一个单元测试从整个系统中单独检验产品程序代码的『一个单元』并检查其得到的结果是否是预期的。要测试的『一个单元』其大小是依据一组连贯的功能的大小及 介于一个类别及一个包(package)之间实际上的变化(varies)。其目的是在整合程序代码到系统的其余部分之前先测试以便找出程序代码中的臭虫 (bugs)。Junit等支持在Java程序代码中撰写单元测试。
    在整合之前于系统其它部分隔离起来抓虫的理由是因为那是比较容易的找到臭虫(亦即比较快且便宜)及比较容易修正问题(并显示其解决方式是可行的)。
    单元测试对于在初始整合一小部分程序代码以及整合其余的改变之前提供了一些利益。如果有人需要变动现有的程序代码,事实上单元测试仍然可以让他对于其最后 的程序代码更有信心;即他的改变不会破坏任何东西。愈好的单元测试让人愈有信心--理想上测试必须在加入的新功能前也随之更新。
    2.        谁来撰写单元测试及何时撰写单元测试
    程序代码测试可能是非常乏味的,尤其是测试别人的程序,而当你是一个程序设计师的时候尤甚。但程序设计师喜欢撰写程序,因此为什么不让程序设计师撰写一些程序可以作为测试之用呢?
    当单元测试正确的实作可以帮助程序设计师变的更有生产力,而同时提升开发程序代码的品质。有一点你必须了解的是单元测试应该是开发程序的一部份是很重要 的;而且程序代码的设计必须是可以测试的。目前的趋势是在撰写程序代码之前要先撰写单元测试,并且把焦点放在Java类别的接口及行为上。
    先写测试,再写代码的好处:
    从技术上强制设计师先考虑一个类的功能,也就是这个类提供给外部的接口,而不至于太早陷入它的细节。这是面向对象提倡的一种设计原则。
    好的测试其实就是一个好的文档,这个类使用者往往可以通过查看这个类的测试代码了解它的功能。特别的,如果你拿到别人的一个程序,对他写测试是最好的了解 这个程序的功能的方法。 xp的原则是 make it simple,不是很推荐另外写文档,因为项目在开发过程中往往处于变动中,如果在早期写文档,以后代码变动后还得同步文档,多了一个工作,而且由于项目 时间紧往往文档写的不全或与代码不一致,与其这样,不如不写。而如果在项目结束后再写文档,开发人员往往已经忘记当时写代码时的种种考虑,况且有下一个项 目的压力,管理人员也不愿意再为旧的项目写文档。导致以后维护的问题
    没有人能保证需求不变动,以往项目往往对需求的变动大为头疼,害怕这个改动会带来其它地方的错误。为此,除了设计好的结构以分割项目外(松耦合),但如果 有了测试,并已经建立了一个好的测试框架,对于需求的变动,修改完代码后,只要重新运行测试代码,如果测试通过,也就保证了修改的成功,如果测试中出现错 误,也会马上发现错在哪里。修改相应的部分,再运行测试,直至测试完全通过。
    软件公司里往往存在开发部门和测试部门之间的矛盾:由于开发和测试分为两个部门,多了一层沟通的成本和时间,沟通往往会产生错误的发生。而且极易形成一个 怪圈:开发人员为了赶任务,写了烂烂的代码,就把它扔给测试人员,然后写其它的任务,测试当然是失败的,又把代码拿回去重写,测试就成了一个很头疼的问 题。这种怪圈的根源是责任不清,根据 xp 中的规定:写这个代码的人必须为自己的代码写测试,而且只有测试通过,才算完成这个任务(这里的测试包括所有的测试,如果测试时发现由于你的程序导致别的 组的测试失败,你有责任通知相关人员修改直至集成测试通过),这样就可以避免这类问题的发生。
    简而言之,如果程序设计师要写一段代码:
    先用 junit 写测试,然后再写代码;
    写完代码,运行测试,如果测试失败,
    修改代码,运行测试,直到测试成功。
    如果以后对程序进行修改,优化 ( refactoring ),只要再运行测试代码。如果所有的测试都成功,则代码修改完成。
    3.        单元测试与Java Team开发的结合
    Java下的team开发,一般采用cvs(版本控制) + ant(项目管理) + junit(单元测试、集成测试)的模式:
    每天早上上班,每个开发人员从 cvs server 获取一个整个项目的工作拷贝。
    拿到自己的任务,先用 junit 写今天的任务的测试代码。
    然后写今天任务的代码,运行测试(单元测试),直到测试通过。
    任务完成在下班前一两个小时,各个开发人员把任务提交到cvs server。
    然后由主管对整个项目运行自动测试(集成测试),哪个测试出错,就找相关人员修改,直到所有测试通过。下班。。。
    4.        测试控制工具中要有甚么?
    无论谁来撰写单元测试或何时撰写单元测试,我们的焦点应该放在检验程序代码;主要是在于产生错误的风险。如果设计文件包含被测试对象的使用情节;便可成为 好的测试来源。不管如何,这些情节写得不是很明确;因为这些情节实际上是以设计观点所写的--因此适当的测试应该有对等的情节,换句话说,也就是测试设计 应该尽可能的包含用户实际使用程序时可能产生的动作或者过程。
    另一个测试案例好的来源是在整合后从产品程序代码当中找到的问题,维修问题的处理方式往往值得封装成为测试案例。
    5.        为什么要使用Junit等工具呢?
    前面的论述说明为什么我们需要测试控制工具,但为什么我们使用Junit这些工具呢?
    首先,它们是完全Free的啦!。
    第二点,使用方便。
            在你提升程序代码的品质时JUnit测试仍允许你更快速的撰写程序
    那听起来似乎不是很直觉,但那是事实。当你使用JUnit撰写测试,你将花更少的时间除虫,同时对你程序代码的改变更 俱有信心。这个信心让你更积极重整程序代码并增加新的功能。没有测试,对于重整及增加新功能你会变得没有信心;因为你不知道有甚么东西会破坏产出的结果。 采用一个综合的测试系列,你可以在改变程序代码之后快速的执行多个测试并对于你的变动并未破坏任何东西感到有信心。在执行测试时如果发现臭虫,原始码仍然 清楚的在你脑中,因此很容易找到臭虫。在JUnit中撰写的测试帮助你以一种极 大(extreme)的步伐撰写程序及快速的找出缺点。
            JUnit非常简单
    撰写测试应该很简单--这是重点!如果撰写测试太复杂或太耗时间,便无法要求程序设计师撰写测试。使用JUnit你可以快速的撰写测试并检测你的程序代码 并逐 步随着程序代码的成长增加测试。只要你写了一些测试,你想要快速并频繁的执行测试而不至于中断建立设计及开发程序。使用JUnit执行测试就像编译你的程 序代码那么容易。事实上,你应该执行编译时也执行测试。编译是检测程序代码的语法而测试是检查程序代码的完整性(integrity)。
            JUnit测试检验其结果并提供立即的回馈。
    如果你是以人工比对测试的期望与实际结果那么测试是很不好玩的,而且让你的速度慢下来。JUnit测试可以自动执行并且检查他们自己的结果。当你执行测试,你获得简单且立即的回馈; 比如测试是通过或失败。而不再需要人工检查测试结果的报告。
            JUnit测试可以合成一个测试系列的层级架构。
    JUnit可以把测试组织成测试系列;这个测试系列可以包含其它的测试或测试系列。JUnit测试的合成行为允许你组合多个测试并自动的回归(regression)从头到尾测试整个测试系列。你也可以执行测试系列层级架构中任何一层的测试。
            撰写JUnit测试所费不多。
    使用Junit测试框架,你可以很便宜的撰写测试并享受由测试框架所提供的信心。撰写一个测试就像写一个方法一样简单;测试是检验要测试的程序代码并定义 期望的结果。这个测试框架提供自动执行测试的背景;这个背景并成为其它测试集合的一部份。在测试少量的投资将持续让你从时间及品质中获得回收。
            JUnit测试提升软件的稳定性。
    你写的测试愈少;你的程序代码变的愈不稳定。测试使得软件稳定并逐步累积信心;因为任何变动不会造成涟漪效应而漫及整个软件。测试可以形成软件的完整结构的胶结。
            JUnit测试是开发者测试。
    JUnit测试是高度区域性(localized)测试;用以改善开发者的生产力及程序代码品质。不像功能测试(function test)视系统为一个黑箱以确认软件整体的工作性为主,单元测试是由内而外测试系统基础的建构区块。开发者撰写并拥有JUnit测试。每当一个开发反复 (iteration)完成,这个测试便包裹成为交付软件的一部份 提供一种沟通的方式,「这是我交付的软件并且是通过测试的。」
            JUnit测试是以Java写成的。
    使用Java测试Java软件形成一个介于测试及程序代码间的无缝(seamless)边界。在测试的控制下测试变成整个软件的扩充同时程序代码可以被重整。Java编译器的单元测试静态语法检查可已帮助测试程序并且确认遵守软件接口的约定。
    一段测试的程序代码无法单独的执行,它需要是执行环境的一部份。同时,它需要自动执行的单元测试--譬如在系统中周期性的执行所有的测试以证明没有任何东 西被破坏。由于单元测试需要符合特定的准则:一个成功的测试不应该是人工检查的(那可要到天荒地老了啊),一个未通过测试的失败应可以产出文件以供诊断修 改。而Junit可以提供给我们这些便利.。这样所有测试开发者所需撰写的只是测试码本身了。跟optimizeit、Jtest那些昂贵而又超级麻烦的 tool比较起来,其利昭然可见!


  • 数据库的单元测试。

    2007-09-16 13:48:33

    数据库的单元测试。
    这些笔录是我关于已完成的数据库功能测试的一些心得。其中的例子是用java语言编写的,但我认为这些想法对于大多数编程环境都普遍适用。当然,我仍致力于寻找更佳的解决方案。
      现实的问题是这样的:你有一个SQL数据库,一些存储过程和一个介于应用程序和数据库之间的中间层。你怎样在其中插入测试代码从而保证在数据库中数据存取功能的实现?
    一、 为什么会有这样的问题?
      我猜想有些,可能不完全是大多数的数据库开发过程都是这样的:建立数据库,编写存取数据到数据库的代码,编译并运行,用一个查询语句查验所列的数据是 否正确显示。如果能正确显示那就大功告成了。 然而,这种靠眼睛来检测的弊端在于:你不经常进行这样的检验,而且这种检验是不完全的。存在这样的可能性,当你对系统进行了修改,过了几个月后,你无意中 破坏了系统,从而导致数据的丢失。作为一个编程人员,你可能不会花很多时间来检查数据本身,这就使错误的数据要经过较长的时间才能暴露出来。我曾经参与一 个建立网站的项目,该项目中在注册时有一个必填数据在大半年中没有被发现未实际输入进数据库。尽管公司市场部曾经提出他们需要这一信息,但因为这项数据从 来没有人去看它,直接导致了这一问题在很长时间内没有被发现。 自动化测试,由于它能经常测试而且测试范围较广,降低了数据丢失的风险。我发现它能使我更心安理得地休息。当然,自动化测试还有其他一些好处,他们本身就 是代码编写的范例,也可以作为文档,便于你修改别人编写的原始程序,从而减少检测所需的时间。
    二、 什么是我们所谈论的测试?
      设想有一个非常简单的用户数据库,包括用户电子信箱和一个标志,用来指示邮件地址是否被弹回。你的数据库程序应该包括插入、修改、删除和查询等方法
      插入方法会调用一个存储过程将数据写入数据库。为了叙述方便,这里省去了一些细节,大致的程序如下所示:
    public class UserDatabase
    {
    ...
    public void insert(User user)
    {
    PreparedStatement ps = connection.prepareCall("{ call User_insert(?,?) }");
    ps.setString(1, user.getEmail());
    ps.setString(2, user.isBad()); // In real life, this would be a boolean.
    ps.executeUpdate();
    ps.close();
    }
    ...
    }
      而我认为的测试代码应为:
    public class TestUserDatabase extends TestCase
    {
    ...
    public void testInsert()
    {
    // Insert a test user:
    User user = new User("some@email.address");
    UserDatabase database = new UserDatabase();
    database.insert(user);
    // Make sure the data really got there:
    User db_user = database.find("some@email.address");
    assertTrue("Expected non-null result", db_user != null);
    assertEquals("Wrong email", "some@email.address", db_user.getEmail());
    assertEquals("Wrong bad flag", false, db_user.isBad());
    }
    ...
    }
      可能你还有更多测试代码。(注意一些测试,例如对date类的测试)。
      assertTrue和assertEquals方法进行条件测试。如果测试失败,他们将返回诊断消息。其重点是这些测试都基于一个测试框架自动执 行,并给出测试成败的标志。这些测试都基于用java语言编写的测试框架Junit类(程序附后)。这一框架也能适应其他诸如C, C++, Perl, Python, .NET (all languages), PL/SQL, Eiffel, Delphi, VB等语言环境。
      下一个问题就是:我们有测试,但我们怎样保证测试数据和实际数据能严格区分?


    三、 不同的鉴别方法
      在开始之前,我必须指出你最好有一个测试用的数据库,你可能更想在非正式的数据库中实践我讲的东西。
      第一种方法是手工在数据库中输入一些预先知道的测试性数据,例如在邮件地址中输入“testuser01@test.testing”。如果你正在测试数据库的查询功能,你能预先知道,比如说有五个,数据库记录是以“@test.testing”结尾的。
      由以上方式插入的数据必须由测试本身进行必要的维护。例如,测试必须负责删除所建立的测试数据,而避免对实际数据进行操作,从而保证整个数据库处于完好状态。
      这种方法还是存在以下问题:
      你不得不和其他编程人员进行数据协调——假设他们也有他们自己的测试数据库。
      在数据库中有些特殊的数据并不正确,如一些特别的邮件地址和被保留饿编号前缀。
      在某些情况下,你将不能用一些特殊的数据来区分测试数据和实际数据,这就比较棘手。例如,某条数据由一些整数型字段构成,而作为测试用的数值都看起来较为合理。
      你的测试只限于你为测试所保留的某些特殊值,这意味着你将小心地选择那些特殊值。
      如果数据对时间敏感,那对数据库的维护将更为困难。例如,数据库中有产品销售提议,而该提议只在明确的时间段里有效。 我曾经试着做过修改。例如,在数据库中增加“is_test”字段作为区分测试数据的标志,从而避免特殊值的问题。但由此带来的问题是,你的测试代码将只 测试那些标记为测试的数据,而你的正式代码却要处理那些未标记为测试的数据。如果你的测试在这方面有区别,你事实上并不在测试同一代码。
    四、 你需要四个数据库
      有些想法认为一个好的测试是足够充分的并能建立测试所需要的全部数据。如果你能在测试进行前就明确知道数据库所处的状态,测试可以进行一些简化。一个简化的方法是建立一个独立的单元测试数据库用于测试程序,测试程序在开始进行前清除测试数据库中的全部数据。
      在代码中,你可以编写一个dbSetUp方法,如下所示:
    public void dbSetUp()
    {
    // Put the database in a known state:
    // (stored procedures would probably be better here)
    helper.exec("DELETE FROM SomeSideTable");
    helper.exec("DELETE FROM User");

    // Insert some commonly-used test cases:
    ...
    }
      任何数据库测试程序都将在做任何事前首先调用dbSetUp方法,它将使测试数据库处于一种已知状态(大部分情况下是空数据库状态)。这种做法具有以下的优点:
      所有的测试数据都在代码层和其他编程人员进行交流,因此没有必要进行外部测试数据协调。
      无须测试用的特殊数据的介入。
      简单而容易理解的一种方法。
      在每一次测试前删除和插入数据可能会花较多时间,但是由于测试用的数据量相对较小,我认为这种方法比较快捷,特别是在测试一个本地数据库时。
      这种做法不利的一面是你需要至少两个数据库。但是请记住,他们在必要是都可以在同一个服务器上运行。采用这种方法,我用了四个数据库,另外两个在紧急关头时使用,具体如下:
      1. 实际使用数据库,包含实际数据。在这个数据库中不进行测试,确保数据的完整性。
      2. 你的本地开发数据库,用来进行大部分的测试。
      3. 一个加入一定量数据的本地开发数据库,可能和其他编程人员共享,用来运行应用程序并检测是否能在实际使用的数据库上运行,而不是照搬实际使用数据库中的全 部数据。从严格意义上说你可能并不需要这一数据库,但这一数据库能确保应用程序在有大量数据的数据库中顺利运行。
      4. 一个发布数据库,或称集成数据库,用来在正式发布前进行一系列测试,从而确保对所有本地数据库的修改都得到确认。如果你一个人开发,你可以省略这个数据 库,但你必须确保所有对数据结构和存储过程的修改都在实际使用数据库中得到确认。 在有多个数据库的情况下,你要确保不同数据库间结构的同步:如果你在测试数据库中改变表的定义或存储过程,你必须记得在实际使用的服务器上进行同样的修 改。发布数据库的作用就是提醒你进行这些修改。另外,我发现如果代码控制系统能将提交时的注释用邮件形式自动发给整个开发组,那将给团队开发带来较大帮 助。CVS就能做到这一点,我希望你能利用这一功能。


    五、 在合适的数据库中进行测试
      在这种情况下,你必须连接正确的数据库。在实际使用数据库中进行测试有可能删除所有的有用数据,这点令我十分害怕。
      有几种办法能避免此类悲剧的发生。例如,比较普遍的做法是将数据库连接设置记录在初始文件中,从而明确哪一个是测试数据库。你也可以通过初始文件进行本地数据库的测试,而用其他指定方法连接实际使用数据库。
      在java代码中,初始文件可能如下所示;
    myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase
      这一连接字符串用来连接数据库。你可以添加第二个连接字符串来区分测试数据库:
    myapp.db.url=jdbc:mysql://127.0.0.1/mydatabase
    myapp.db.testurl=jdbc:mysql://127.0.0.1/my_test_database
      在测试代码中,你可以检查并确保在连接到测试数据库后应用程序才能继续运行:
    public void dbSetUp()
    {
    String test_db = InitProperties.get("myapp.db.testurl");
    String db = InitProperties.get("myapp.db.url");
    if (test_db == null)
    abort("No test database configured");
    if (test_db.equals(db))
    {
    // All is well: the database we're connecting to is the
    // same as the database identified as "for testing"
    }
    else
    {
    abort("Will not run tests against a non-test database");
    }
    }
      另一个技巧是,如果你有一个本地测试数据库,测试程序能通过提供IP地址或主机名进行检测。如果不是“localhost/127.0.0.1”,这就有连接在实际使用数据库上进行测试的风险。
    六、 关于测试日期的体会
      如果你想存储日期信息,你大概想确认你存的日期信息是否正确。请注意以下几点。
      首先先问自己,是谁创建该日期。如果是你的应用程序,那验证比较简单,因为你可以通过查看数据库中的具体日期进行比较。如果是数据库本身创建该日期, 可能作为一个缺省字段,那你可能就会有些问题。例如,你能确保你代码所代表的时区和数据库的时区一致吗?从没有听说过数据库是以格林尼治标准时间为准显示 时间和日期的。你能确保运行应用程序的计算机上的时间和数据库所在计算机上的时间保持一致吗?如果不是,你必须在进行时间的比较时留出一定的误差。
      如果你遇到这些情况,有些事是你可以做的:
      如果你预先知道所用的时区,在测试前将所有日期和时间全部转换成那个时区的日期和时间。
      在比较时间时设立一定的误差,比如说几分钟、几小时或几个月。看上去缺乏说服力,但至少它能捕获诸如日期为空或1970年1月1日等错误。
    七、 总结
      在本文中,我想说:
      单元数据库测试是一件值得做的事;
      如果你能给一系列测试程序一个对应的数据库,测试本身并不非常可怕。
      还有其他方法能解决这一问题。我还不能确信模仿对象(Mock Object)这一方法。就我对这一方法的理解,模仿对象模拟了一个系统中间层(在本文中,是数据库操作系统),使得模仿的数据库总能返回你想要的数据。 我比较欣赏这一概念,它鼓励你对测试进行分层,可能划分成SQL方面的测试和Java语言方面的测试,从而对模仿的ResultSet对象进行测试。
      我比较关注那些能导致一次能使两个或两个以上的数据表产生变化的操作。在这种情况下,用模仿对象方法进行数据库的维护和实现比较困难。当然,我还要找到一种好方法进行数据库中SQL方面的测试,从而确认数据被正确地存储到数据库中。


  • 单元测试指导

    2007-09-16 13:44:24

    单元测试指导
    这是交大慧谷软件测试沙龙二期张华先生与大家分享的内容,很有用的

    单元测试指导

    一、单元测试环境配置测试
    1. 网络连接是否正常
    2. 网络流量负担是否过重
    3. 软件测试平台是否可选
    4. 如果(3),是否在不同的软件测试平台进行软件测试
    5. 所选软件测试平台的版本(包括Service Pack)是否正确
    6. 所选软件测试平台的参数设置是否正确
    7. 所选软件测试平台上正在运行的其它程序是否会影响测试结果
    8. 画面的分辨率和色彩设定是否正确
    9. 对硬件测试平台的要求和支持程度

    二、代码测试
    A 静态测试
    1. 同一程序内的代码书写是否为同一风格
    2. 代码布局是否合理、美观
    3. 程序中函数、子程序块分界是否明显
    4. 注释是否符合既定格式
    5. 注释是否正确反映代码的功能
    6. 变量定义是否正确(长度、类型、存储类型)
    7. 子程序(函数和方法)接受的参数类型、大小、次序是否和调用模块相匹配合
    8. 函数的返回值类型是否正确
    9. 程序中是否引用了未初始化变量
    10. 数组和字符串的下标是否为整数
    11. 数组和字符串的下标是否在范围内(不“越界”)
    12. 进行数组的检索及其它操作中,是否会出现“漏掉一个这种情况”
    13. 是否在应该使用常量的地方使用了变量(例:数组范围检查)
    14. 是否为变量赋予不同类型的值
    15. (14)的情况下,赋值是否符合数据类型的转换规则
    16. 变量的命名是否相似
    17. 是否存在声明过,但从未引用或者只引用过一次的变量
    18. 在特定模块中所有的变量是否都显式声明过
    19. 非(18)的情况下,是否可以理解为该变量具有更高的共享级别
    20. 是否为引用的指针分配内存
    21. 数据结构在函数和子程序中的引用是否明确定义了其结构
    22. 计算中是否使用了不同数据类型的变量
    23. 计算中是否使用了不同的数据类型相同但长度不同的变量
    24. 赋值的目的变量是否小于赋值表达式的值
    25. 数值计算是否会出现溢出(向上)的情况
    26. 数值计算是否会出现溢出(向下)的情况
    27. 除数是否可能为零
    28. 某些计算是否会丢失计算精度
    29. 变量的值是否超过有意义的值
    30. 计算式的求值的顺序是否容易让人感到混乱
    31. 比较是否正确
    32. 是否存在分数和浮点数的比较
    33. 如果(32),精度问题是否会影响比较
    34. 每一个逻辑表达式是否都得到了正确表达
    35. 逻辑表达式的操作数是否均为逻辑值
    36. 程序中的Begin…End和Do…While等语句中,End是否对应
    37. 程序、模块、子程序和循环是否能够终止
    38. 是否存在永不执行的循环
    39. 是否存在多循环一次或少循环一次的情况
    40. 循环变量是否在循环内被错误地修改
    41. 多分支选择中,索引变量是否能超过可能的分支数
    42. 如果(41),该情况是否能够得到正确处理
    43. 全局变量定义和用法在各个模块中是否一致
    44. 是否修改了只作为输入用的参数
    45. 常量是否被作为形式参数进行传递

    B 动态测试
    1. 测试数据是否具有一定的代表性
    2. 测试数据是否包含测试所用的各个等价类(边界条件、次边界条件、空白、无效)
    3. 是否可能从客户那边得到测试数据
    4. 非(3)的情况下,所用的测试数据是否具有实际的意义(客户业务上的)
    5. 是否每一组测试数据都得到了执行
    6. 每一组测试数据的测试结果是否与预期结果一致
    7. 文件的属性是否正确
    8. 打开文件语句是否正确
    9. 输入/输出语句是否与格式说明书所记述的一致
    10. 缓冲区大小与记录长度是否匹配
    11. 使用文件前是否已打开了文件
    12. 文件结束条件是否存在
    13. 产生输入/输出错误时,系统是否进行检测并处理
    14. 输出信息中是否存在文字书写错误和语法错误
    15. 数字输入框是否接受数字输入
    16. (15)的情况下、数字是否按既定格式显示
    17. 数字输入框是否拒绝字符串和“非法”数字的输入
    18. 组合框是否的能够进行下拉选择
    19. 组合框是否能够进行下拉多项选择
    20. 对于可添加数据组合框,添加数据后数据是否能够得到正确显示和进行选择
    21. 列表框是否能够进行选择
    22. 多项列表框是否能够进行多数据项选择
    23. 日期输入框是否接受正确的日期输入
    24. 日期输入框是否拒绝错误的日期输入
    25. 日期输入框在日期输入后是否按既定的日期格式显示日期
    26. 单选组内是否有且只有一个单选钮可选
    27. 如果单选组内无单选钮可选,这种情况是否允许存在
    28. 复选框组内是否允许多个复选框(包括全部可选)可选
    29. 如果复选框组内无复选框可选,这种情况是否允许存在
    30. 文本框及某些控件拒绝输入和选择时显示区域是否变灰或按既定规约处理
    31. 文本框中数据格式(大小、对齐方向、颜色、背景)是否符合规范
    32. 密码输入框是否按掩码的方式显示
    33. 控件是否存在默认输入值,若存在,默认值是否得到显示和提交
    34. Cancel之类的按钮按下后,控件中的数据是否清空复原或按既定规约处理
    35. Submit之类的按钮按下后,数据是否得到提交或按既定规约处理
    36. 异常信息表述是否正确
    37. 软件是否按预期方式处理错误
    38. 文件或外设不存在的情况下是否存在相应的错误处理
    39. 软件是否严格的遵循外设的读写格式
    40. 产生的文件和数据表的格式是否正确
    41. 产生的文件和数据表的计算结果是否正确
    42. 打印的报表是否符合既定的格式
    43. 错误日志的表述是否正确
    44. 错误日志的格式是否正确

    C GUI测试
    1. 窗体是否能够基于相关的输入或菜单命令适当的打开
    2. 窗体是否能够改变大小、移动和滚动
    3. 窗体的数据是否能够利用鼠标、功能键、方向箭头和键盘操作
    4. 当窗体被覆盖并重新调用后,窗体是否能够正确再生
    5. 窗体相关的功能是否可以操作
    6. 是否显示相关的下拉菜单、工具条、滚动条、对话框、按钮、图标和其他控制,既能正确显示又能调用
    7. 显示多窗体时,窗体名称是否能够正确表示
    8. 活动窗体是否能够被反显加亮
    9. 多用户联机时所有窗体是否能够实时更新
    10. 鼠标无规则点击时是否会产生无法预料的结果
    11. 窗体声音及提示是否符合既定编程规则
    12. 窗体是否能够被关闭
    13. 窗体控件的大小、对齐方向、颜色、背景等属性的设置值是否和程序设计规约相一致
    14. 窗体控件布局是否合理、美观
    15. 窗体控件TAB顺序是否从左到右,从上到下
    16. 窗体焦点是否按照编程规范落在既定的控件上
    17. 窗体画面文字(全、半角、格式、拼写)是否正确
    18. 鼠标有多个形状时是否能够被窗体识别(如漏斗状时窗体不接受输入)
  • 单元测试的基本方法[转帖]

    2007-09-16 13:41:25

    单元测试的对象是软件设计的最小单位——模块。单元测试的依据是详细设描述,单元测试应对模块内所有重要的控制路径设计测试用例,以便发现模块内部的错误。单元测试多采用白盒测试技术,系统内多个模块可以并行地进行测试。
        单元测试任务
      单元测试任务包括:1 模块接口测试;2 模块局部数据结构测试;3 模块边界条件测试;4 模块中所有独立执行通路测试;5 模块的各条错误处理通路测试。
      模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。测试接口正确与否应该考虑下列因素:
      1 输入的实际参数与形式参数的个数是否相同;
      2 输入的实际参数与形式参数的属性是否匹配;
      3 输入的实际参数与形式参数的量纲是否一致;
      4 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
      5 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
      6调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
      7 调用预定义函数时所用参数的个数、属性和次序是否正确;
      8 是否存在与当前入口点无关的参数引用;
      9 是否修改了只读型参数;
      10 对全程变量的定义各模块是否一致;
      11是否把某些约束作为参数传递。
      如果模块内包括外部输入输出,还应该考虑下列因素:
      1 文件属性是否正确;
      2 OPEN/CLOSE语句是否正确;
      3 格式说明与输入输出语句是否匹配;
      4缓冲区大小与记录长度是否匹配;
      5文件使用前是否已经打开;
      6是否处理了文件尾;
      7是否处理了输入/输出错误;
      8输出信息中是否有文字性错误;
      检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
      1 不合适或不相容的类型说明;
      2变量无初值;
      3变量初始化或省缺值有错;
      4不正确的变量名(拼错或不正确地截断);
      5出现上溢、下溢和地址异常。
      除了局部数据结构外,如果可能,单元测试时还应该查清全局数据(例如FORTRAN的公用区)对模块的影响。
      在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。此时设计测试用例是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。此时基本路径测试和循环测试是最常用且最有效的测试技术。计算中常见的错误包括:
      1 误解或用错了算符优先级;
      2混合类型运算;
      3变量初值错;
      4精度不够;
      5表达式符号错。
      比较判断与控制流常常紧密相关,测试用例还应致力于发现下列错误:
      1不同数据类型的对象之间进行比较;
      2错误地使用逻辑运算符或优先级;
      3因计算机表示的局限性,期望理论上相等而实际上不相等的两个量相等;
      4比较运算或变量出错;
      5循环终止条件或不可能出现;
      6迭代发散时不能退出;
      7错误地修改了循环变量。
      一个好的设计应能预见各种出错条件,并预设各种出错处理通路,出错处理通路同样需要认真测试,测试应着重检查下列问题:
      1输出的出错信息难以理解;
      2记录的错误与实际遇到的错误不相符;
      3在程序自定义的出错处理段运行之前,系统已介入;
      4异常处理不当;
      5错误陈述中未能提供足够的定位出错信息。
      边界条件测试是单元测试中最后,也是最重要的一项任务。众的周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。
  • 单元测试的基本方法

    2007-09-16 13:32:02

    单元测试的基本方法
      单元测试的对象是软件设计的最小单位——模块。单元测试的依据是详细设描述,单元测试应对模块内所有重要的控制路径设计测试用例,以便发现模块内部的错误。单元测试多采用白盒测试技术,系统内多个模块可以并行地进行测试。

    单元测试任务


      单元测试任务包括:1 模块接口测试;2 模块局部数据结构测试;3 模块边界条件测试;4 模块中所有独立执行通路测试;5 模块的各条错误处理通路测试。


      模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。测试接口正确与否应该考虑下列因素:
      1 输入的实际参数与形式参数的个数是否相同;
      2 输入的实际参数与形式参数的属性是否匹配;
      3 输入的实际参数与形式参数的量纲是否一致;
      4 调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
      5 调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
      6调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
      7 调用预定义函数时所用参数的个数、属性和次序是否正确;
      8 是否存在与当前入口点无关的参数引用;
      9 是否修改了只读型参数;
      10 对全程变量的定义各模块是否一致;
      11是否把某些约束作为参数传递。


      如果模块内包括外部输入输出,还应该考虑下列因素:
      1 文件属性是否正确;
      2 OPEN/CLOSE语句是否正确;
      3 格式说明与输入输出语句是否匹配;
      4缓冲区大小与记录长度是否匹配;
      5文件使用前是否已经打开;
      6是否处理了文件尾;
      7是否处理了输入/输出错误;
      8输出信息中是否有文字性错误;


      检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
      1 不合适或不相容的类型说明;
      2变量无初值;
      3变量初始化或省缺值有错;
      4不正确的变量名(拼错或不正确地截断);
      5出现上溢、下溢和地址异常。


      除了局部数据结构外,如果可能,单元测试时还应该查清全局数据(例如FORTRAN的公用区)对模块的影响。


      在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。此时设计测试用例是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。此时基本路径测试和循环测试是最常用且最有效的测试技术。计算中常见的错误包括:
      1 误解或用错了算符优先级;
      2混合类型运算;
      3变量初值错;
      4精度不够;
      5表达式符号错。


      比较判断与控制流常常紧密相关,测试用例还应致力于发现下列错误:
      1不同数据类型的对象之间进行比较;
      2错误地使用逻辑运算符或优先级;
      3因计算机表示的局限性,期望理论上相等而实际上不相等的两个量相等;
      4比较运算或变量出错;
      5循环终止条件或不可能出现;
      6迭代发散时不能退出;
      7错误地修改了循环变量。


      一个好的设计应能预见各种出错条件,并预设各种出错处理通路,出错处理通路同样需要认真测试,测试应着重检查下列问题:
      1输出的出错信息难以理解;
      2记录的错误与实际遇到的错误不相符;
      3在程序自定义的出错处理段运行之前,系统已介入;
      4异常处理不当;
      5错误陈述中未能提供足够的定位出错信息。


      边界条件测试是单元测试中最后,也是最重要的一项任务。众的周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。


    单元测试过程


      一般认为单元测试应紧接在编码之后,当源程序编制完成并通过复审和编译检查,便可开始单元测试。测试用例的设计应与复审工作相结合,根据设计信息选取测试数据,将增大发现上述各类错误的可能性。在确定测试用例的同时,应给出期望结果。


      应为测试模块开发一个驱动模块(driver)和(或)若干个桩模块(stub),下图显示了一般单元测试的环境。驱动模块在大多数场合称为“主程序”,它接收测试数据并将这些数据传递到被测试模块,被测试模块被调用后,“主程序”打印“进入-退出”消息。


      驱动模块和桩模块是测试使用的软件,而不是软件产品的组成部分,但它需要一定的开发费用。若驱动和桩模块比较简单,实际开销相对低些。遗憾的是,仅用简单的驱动模块和桩模块不能完成某些模块的测试任务,这些模块的单元测试只能采用下面讨论的综合测试方法。


      提高模块的内聚度可简化单元测试,如果每个模块只能完成一个,所需测试用例数目将显著减少,模块中的错误也更容易发现。





  • 如何监控Tomcat服务器

    2007-09-15 13:05:01

    在进行性能测试时,一般都需要对应用服务器进行监控,监控的指标包括应用服务器的JVM使用状况、可用连接数、队列长度等信息。商业的应用服务器如WebLogic、WebSphere等都提供了Console对这些指标进行监控,在性能测试时可以很容易观察这些指标的情况。

    Tomcat作为在国内得到广泛应用的J2EE服务器,在不少项目中都得到了使用。Tomcat小巧灵活、配置简单,非常适合小的WEB应用使用。但在对使用Tomcat的应用系统进行性能测试时,最头疼的问题就是不能获得应用服务器的相关性能指标数据。

    其实,从Tomcat 5.0开始,Tomcat就已经为自己提供了一个用于监控应用服务器性能指标的servelet —— status servelet。安装完Tomcat 5之后,通过访问 http://yourhost:port/manager/status 就可以获得当时的应用服务器监控数据。

    当然,为了安全起见,Tomcat 5在缺省安装时是不允许用户直接访问 http://yourhost:port/manager/status 的,访问的时候会给出一个403(forbidden)的错误信息。在Tomcat的Manual里说明了允许用户访问的方法:
    1. 转到Tomcat的安装目录下;
    2. 修改conf/tomcat-users.xml文件,在其中加入一行
         <user username="servermon" password="passwd" roles="manager"/>

    这样就可以在访问 http://yourhost:port/manager/status 时给出 servermon 的用户名与口令,查看到应用服务器的相关性能指标数据。

  • 学会使用Linux性能分析工具

    2007-09-15 13:03:06

    Linux在具有高稳定性、可靠性的同时,具有很好的可伸缩性和扩展性,能够针对不同的应用和硬件环境调整,优化出满足当前应用需要的最佳性能。因此企业在维护Linux系统、进行系统调优时,了解系统性能分析工具是至关重要的。
      在Linux下有很多系统性能分析工具,比较常见的有top、free、ps、time、timex、uptime等。下文将介绍几个较为重要的性能分析工具vmstat、iostat和sar及其使用。

      用vmstat监视内存使用情况

      vmstat是Virtual Meomory Statistics(虚拟内存统计)的缩写,可对操作系统的虚拟内存、进程、CPU活动进行监视。它是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。

      vmstat的语法如下:

      vmstat [-V] [-n] [delay [count]]

      其中,-V表示打印出版本信息;-n表示在周期性循环输出时,输出的头部信息仅显示一次;delay是两次输出之间的延迟时间;count是指按照这个时间间隔统计的次数。对于vmstat输出各字段的含义,可运行man vmstat查看。

      用iostat监视I/O子系统情况

       iostat是I/O statistics(输入/输出统计)的缩写,iostat工具将对系统的磁盘操作活动进行监视。它的特点是汇报磁盘活动统计情况,同时也会汇报出 CPU使用情况。同vmstat一样,iostat也有一个弱点,就是它不能对某个进程进行深入分析,仅对系统的整体情况进行分析。

      iostat的语法如下:

    iostat [ -c | -d ] [ -k ] [ -t ] [ -V ] [ -x [ device ] ] [ interval [ count ] ]

       其中,-c为汇报CPU的使用情况;-d为汇报磁盘的使用情况;-k表示每秒按kilobytes字节显示数据;-t为打印汇报的时间;-v表示打印出 版本信息和用法;-x device指定要统计的设备名称,默认为所有的设备;interval指每次统计间隔的时间;count指按照这个时间间隔统计的次数。

      iostat一般的输出格式如下:

    Linux 2.4.18-18smp (builder.linux.com) 2003年03月07日 avg-cpu: %user %nice %sys %idle 4.81 0.01 1.03 94.15 Device: tps Blk_read/s Blk_wrtn/s Blk_read Blk_wrtn dev3-0 30.31 1117.68 846.52 16104536 12197374 dev3-1 7.06 229.61 40.40 3308486 582080

      对于输出中各字段的含义,iostat的帮助中有详细的说明。

      使用sar进行综合分析

      表1 sar参数说明

      选项 功能

      -A 汇总所有的报告

      -a 报告文件读写使用情况

      -B 报告附加的缓存的使用情况

      -b 报告缓存的使用情况

      -c 报告系统调用的使用情况

      -d 报告磁盘的使用情况

      -g 报告串口的使用情况

      -h 报告关于buffer使用的统计数据

      -m 报告IPC消息队列和信号量的使用情况

      -n 报告命名cache的使用情况

      -p 报告调页活动的使用情况

      -q 报告运行队列和交换队列的平均长度

      -R 报告进程的活动情况

      -r 报告没有使用的内存页面和硬盘块

      -u 报告CPU的利用率

      -v 报告进程、i节点、文件和锁表状态

      -w 报告系统交换活动状况

      -y 报告TTY设备活动状况



       sar是System Activity Reporter(系统活动情况报告)的缩写。顾名思义,sar工具将对系统当前的状态进行取样,然后通过计算数据和比例来表达系统的当前运行状态。它的 特点是可以连续对系统取样,获得大量的取样数据;取样数据和分析的结果都可以存入文件,所需的负载很小。sar是目前Linux上最为全面的系统性能分析 工具之一,可以从14个大方面对系统的活动进行报告,包括文件的读写情况、系统调用的使用情况、串口、CPU效率、内存使用状况、进程活动及IPC有关的 活动等,使用也是较为复杂。

      sar的语法如下:

    sar [-option] [-o file] t [n]

      它的含义是每隔t秒取样一次,共取样n次。其中-o file表示取样结果将以二进制形式存入文件file中。

      另一种语法如下:

    sar [-option] [-s time] [-e time] [-i sec] [-f file]

       含义是表示从file文件中取出数据,如果没有指定-f file,则从标准数据文件/var/adm/sa/sadd取数据,其中dd表示当前天。另外,-s time表示起始时间;-e time表示停止时间;-i sec表示取样的时间间隔,如果不指定则表示取文件中所有的数据。对于具体的选项参见表1。

      一般它与-q和-u联合使用,以便对每个CPU的使用情况进行分析,比如运行如下命令:

    sar -q -u 5 1

      将输出如下:

    Linux 2.4.18-18smp (builder.linux.com) 2003年03月07日 09时46分16? CPU %user %nice %system %idle 09时46分21? all 0.20 0.00 0.00 99.80 09时46分16? runq-sz plist-sz ldavg-1 ldavg-5 09时46分21? 0 91 0.00 0.00 Average: CPU %user %nice %system %idle Average: all 0.20 0.00 0.00 99.80 Average: runq-sz plist-sz ldavg-1 ldavg-5 Average: 0 91 0.00 0.00

      由于sar命令太复杂,只有通过熟练使用才能了解每个选项的含义,对于sar输出中每个字段的含义运行man sar命令可以得到详细的解释。

  • 如何用LR监视服务器LINUX的方法

    2007-09-15 13:01:28

    一、在服务器上安装rstatd守护进程
    安装步骤:
    1. 从网上下载rstatd
    2. 将该文件放到/home/user目录下
    3. chmod 777 rpc.rstatd----改变该文件读写的权限,拥有所有权限。
    4. chmod 777 configure ---同上
    5. ./configure ---配置
    6. make ---编译
    7. make install ---安装
    8. rpc.rstatd ---启动rstatd进程

    二、在lr中配置
    从LR里面add measurement, 填写linux机器的IP,出现所有unix/linux的计数器,包括cpu的,mem的,disk,network的。介绍几个常用的:
    average load :在过去的1分钟,的平均负载
    cpu utilization: cpu的使用率
    disk traffic: disk传输率
    paging rate: 每秒从磁盘读到物理内存,或者从物理内存写到页面文件的内存页数
    Swap-in rate: 每秒交换到内存的进程数
    Swap-out rate: 每秒从内存交换出来的进程


    补充一些常见的问题及处理方法:
    1、在执行配置或安装命令过程中出现“拒绝的权限”的提示;
    答:是由于文件的权限引起的,应该给当前用户所有文件的“777”权限,即完全控制权限。

    2、安装好后从LoadRunner中看不到信息,但是没有报错;
    答:可能是返回的信息值比较小,所以在图中几乎看不到,例如:如果没有运行程序的话,CPU的使用率接近于0,所以在监视图中看不到变化。也有可能是采样的频率过大,可以在图表中设置没1秒获取一次信息,这样界面就刷新的比较及时了。

    3、监视一段时间后LoadRunner中提示有错误发生不能继续监视到信息;
    答:可能是由于CPU长时间处于高负荷状态,而导致系统自动关闭了该服务。可以在LoadRunner中重新加一次计数器,并且设置取样的时间稍长一点,就会避免这种情况。

    4、以前用LoadRunner监视都是成功的,但是再次监视不到信息;
    答:有可能是由于系统重新启动,而没有打开rstatd守护进程。可以手工重新打开一次,使用命令“rpc.rstatd”,另外可以使用“rpcinfo -p”命令来查看当前系统是否已经启动了rstatd守护进程。

  • 对LR回放中highest severity level was"ERROR"的解决方法

    2007-09-15 12:59:04

    在LR中录制脚本时有如下问题:
    在录制时一切正常,而回放时提示类似如下错误:
    Action.c(41): Error -27979: Requested form not found    [MsgId: MERR-27979]
    Action.c(41): web_submit_form highest severity level was "ERROR",  0 body bytes, 0 header bytes  [MsgId: MMSG-27178]"
     这时在tree view中看不到此组件的相关URL。

     处理方法如下: 1, 打开recording options,在internet protocol下的recording中选择recording level为HTML-based scrīpt,点击HTML Advanced,选择scrīpt type为A scrīpt containing explicit.即可。 2, 选择使用URL_based scrīpt录制。

     另外,附上帮助中的这个错误代码的说明:

    Message Code 27979

    Requested form not found

    The form was not found in the page received from the server. Possible reasons: (i) The current or a previous HTML page was changed after the scrīpt was recorded. (ii) A previous request navigated to a wrong page or failed. (iii) One or more web_submit_form arguments are missing or incorrect (especially for manually coded, parameterized, or correlated functions). (iv) The server returned an unexpected page (e.g., under excessive load).

    Troubleshooting If the current or previous HTML page was changed, look for the correct properties of the form used in the text (e.g., action), and change it in the scrīpt. If your snapshots and extended log are disabled, enable them and run the scrīpt again.
    (i) To enable Snapshots: Select Tools > General Options > Correlation, and check the Save correlation information during replay box.
    (ii) To enable Extended Log: Select Run-time Settings > General: Log, and check Enable logging, Always send messages, Extended log, Data returned by server, Advanced trace. Compare the record and replay snapshots for each step in the scrīpt from the beginning, and locate the first difference. If you identify a difference in the snapshots, locate the corresponding HTTP request in both the recording log and the extended log. Compare the requests and, if they are different, have the replay issue exactly the same request. This can be done, for example, by adding web_add_header (for adding missing headers or adjusting existing ones), web_remove_auto_header (for removing extra headers), and web_add_cookie (for missing cookies). If some correlation is missing, (e.g., the recording and replay runs have different session IDs), use the correlation tool to locate and handle such cases.
Open Toolbar