发布新日志

  • QTP常见运行错误问题解答(转)

    2011-04-28 14:48:46

    出处:http://blog.163.com/lilei_0207/blog/static/306754920079242641852/

    1. 不能识别对象(Cannot identify object

    出现了这个错误时,就表示当前被测程序的窗口中没有符合条件的对象。

    出现这种错误,可能的原因有以下两种:

    >>> 对象此时在程序中没有显示或不存在。

    >>> 在程序中存在该对象,但是它的描述在后来发生了改变,导致与对象仓库中存储的信息不匹配,从而QTP无法识别对象。

    如果你正遇到“Cannot identify object”的问题,解决方法见1.1

    1.1 如何去识别对象描述(Identifying Test Object Description Problems

        如果你能在被测程序中看到对象,但仍出现了“Cannot Identify Object”错误信息,这就表示仓库中对象的描述与程序中对象的描述一定存在着差异。

        解决对象描述问题的第一步是找到差异,最简单的方法是将仓库中对象的属性值与程序中对象的属性值进行比较。(仓库中对象的属性值可在Object Repository窗口中查看,程序中对象的属性值可用Object Spy功能查看。)

    比较仓库中对象的属性值与程序中对象的属性值的操作见1.3

    在完成比较后,是否发现存在差异?

    如果是,请参考1.4

    如果否,请参考1.5 

    1.2 处理对象丢失问题(Solving Missing Object Problems

    在运行脚本时,QTP尝试进行某种操作,而该操作的对象在程序中却没有出现,出现这种情况的原因有以下几种:

    >>> 对象不再存在。对象已经被从程序中删除。

    解决方法:见1.2.1

    >>> 对象还没有装载。

    解决方法:见1.2.2

    >>>当前的程序页面(或窗口)不正确,不是对象所在的页面(或窗口)。

    解决方法:见1.2.3

    >>>前一个步骤没有正确执行。

    解决方法:见1.2.3

    1.2.1 解决对象不再存在的问题

    如果对象已经不再在被测程序中存在,则应在脚本中修改或删除相关步骤。

    1.2.2 解决对象装载超时的问题

    如果对象丢失的原因是因为没有足够的时间装载,尝试以下解决方案:

    a.对于Web对象,增加Browser Navigation TimeoutFile>Setting>Web页签)时间。

    b.在包括该对象的步骤前使用Wait语句,让QTP在执行该步骤之前等待一段时间。

    1.2.3 检查前面步骤的执行情况

    如果是因为打开了错误的页面(或窗口)导致对象丢失问题,或因为前一步骤执行措误导致对象丢失问题,则请按以下方法检查原因:

    a. 如果怀疑在完成脚本后,被测程序又发生了改变,则检查对象的继承关系以及对象描述。参考1.1

    b. 如果怀疑脚本错误,则检查脚本的每一个步骤。你可能是遗失了某个步骤,也可能是使用了不正确的方法或参数

    1.3 测试对象的属性值与Run-time对象的属性值的比较(Comparing Test Object and Run-Time Object Property Values

    根据以下的步骤比较测试对象与Run-time对象的属性值:

    1) 进入Obecjt Repository窗口(Resources>Object Repository),选择对象。

    2) 用笔记下对象的class以及它的各个属性及属性值。

    3) 打开被测程序,并打开包含被测对象的页面或窗口。

    4) QTP中选择菜单Tools>Object Spy或点击Object Spy按钮,打开Object Spy对话框。

    5) 选择“Test Object Properties”选项。

    6) 点击右上角的按钮(带有手图标的),这时QTP窗口以及Object Spy对话框都被最小化。

    7) 在程序页面(或窗口)中点击目标对象,恢复Object Spy对话框,并在对话框中显示对象及其父对象(以树的形式显示),并在Properties页签中显示当前对象的属性及属性值。

    8) 这时Object Spy对话框的Properties页签中显示Run-time对象的所有属性,在此查看对象的class,以及它的属性及属性值,并将它与第2步中记下的内容进行比较。 

    1.4 解决对象描述存在的问题(Solving Object Descrīption Problems

        如果发现仓库中对象的属性值与程序中对象的属性值存在不同,你应该判断这个不同是个别对象的问题,还是其它同类对象也存在相同的问题。

    是所有(或多个)同类对象都存在问题吗?

    如果是,则参考1.6

    如果否,则参考1.7 

    1.5 关于父对象描述的识别问题(Identifying Parent Object Descrīption Problems

    对象的识别还与它所继承的父对象有关。

        如果你能在被测程序中看到对象,而且程序中对象的属性值与仓库中对象的属性值也是一致的,但仍然遭遇到了“Cannot identify object”错误,则这个错误可能与它的父对象有关(如仓库中父对象的属性值与程序中父对象的属性值不一致),也可能是仓库中的对象与程序中对象的继承关系不相同导致的。

    要判断是继承关系中的哪个父对象出现了问题,请偿试下面的方法之一:

    >>>重新录制对象,比较新旧对象的父对象。

    欲了解如何完成比较,参考1.8

    >>>对继承关系中的每个父对象,分别插入一个Exist语句,并运行该部分脚本。

    欲了解如果创建Exist语句,参考1.9

    注:你也可以使用Object Repository窗口中的Highlight in Application功能,在被测程序中定位对象。

    一旦找到了存在问题的父对象,接下来偿试以下方法:

    >>>修复存在问题的父对象的描述。方法见1.4

    >>>在专家视图的模式下,查找所有继承存在问题的实例并进行修正。

    举例:1.8.1 

    1.6 解决某类对象的识别问题(Soving Object Identification Problems for a Test Object Class

        如果你发现某类对象的对象描述对于被测程序来说都不是很合理,或你预期到对象描述中的某属性值是经常变化的,你可以在Object Identification对话框中改变该类的识别属性的设置,或定义该类的Smart Identification设置并启用Smart Identification机制,这样QTP就可以唯一识别对象了。

    1.7 解决单个对象的描述问题(Solving Individual Test Object Description Problems

    选择下列方法之一来解决对象的描述问题:

    >>>如果被测程序中对象描述发生了改变,并且你也清楚改变的内容,并且该改变是永久性或长期性的,你可以直接手工修改仓库中对象描述中的相关属性值。

    >>>如果被测程序中的对象的属性值依赖于前面的步骤或其它对象,则将该属性值参数化,这样就可以使用其它步骤的输出值为属性值。

    >>>如果属性值的组成部分中,部分是固定的,部分是动态改变的,则可以将属性值设计为正则表达式。

    >>>如果属性值是遵循某种规则变化的,或者是不可预期的,则从对象描述中移除该属性,并向对象描述中添加一个或多个属性以便于QTP进行对象识别。

    >>>If you can only access the information on the property values during the run session, you can create and use functions that use programmatic descriptions to identify the object using property values retrieved earlier in the run session.

    For more information on programmatic descriptions, see Using Programmatic Descriptions.

    >>>如果在录制过程中对象是唯一的,但现在程序中出现了两个或多个描述相同的对象,但是它们在页面(或窗口)中的位置不相同,则应在Object PropertiesObject Repository窗口中,向对象描述中添加一个ordinal identifierindexlocation)。

     

    1.8 重新录制对象,以判断父对象描述是否存在问题(Re-recording an Object to Identify Parent Object Description Problems

    根据下面的指引,来判断父对象的问题。

    通过重新录制对象,来判断它的父对象是否存在问题:

    1) 打开浏览器或程序,来到包含被测对象的页面或窗口。

    2) 在关键字视图模式,选择最后一个组件(component),或在专家视图模式,将光标放在最后一个脚本步骤的下面。

    3) 点击Record按钮,或选择Automation>Record

    4) 点击(或操作)页面或窗口中的目标对象。

    5) 点击Stop按钮,或选择Automation>Stop,完成步骤添加。

    6) 右击新添加的步骤,并选择Object Properties右键菜单。

    7) Object Properties对话框中,点击Repository,打开Object Repository窗口,并选中了新对象。注意记下该对象的继承关系。

    8) Object Repository窗口中找到旧的对象(即存在问题的对象),将它的继承关系与新对象的继承关系进行比较。

    通过比较,可以检查到新旧对象是父对象的描述存在不同,还是它们的继承关系根本就不同。

    a. 如果是父对象的描述问题,则修改父对象的描述。参见1.4.

    b. 如果是对象的继承关系不正确(即具有不同的父对象)引起的问题,则在脚本中找到所有使用该继承关系的实例,将它们替换为正确的继承关系。

    举例:参见1.8.1

    1.8.1 举例

        假设有一个带有Frame的网站,你录制了一个操作针对Image对象“Poster”的操作步骤。在Object Repository窗口,你可以Image对象的继承关系如下所示:

    MyCompanyBrowser

        MyCompanyPage

            Main(Frame)

                 Poster(image)

        当你运行脚本时,网页看起来没有什么不同,但是识别Image对象时却出现了“Cannot identiry object”的错误提示。当你重新录制操作步骤,然后在Object Repository窗口中查看新的Image对象时,发现该对象的继承关系如下所示:

    MyCompanyBrowser

        MyCompanyPage

             Poster(image)

        从中可以看出,Frame已经从Web Page中移除,所以尽管Image对象“Poster”的描述没有发生任何改变,但它的继承关系已经改变了。

        Object Repository窗口,你可以看到新旧Image对象“Poster”是两个完全不同的对象:旧的位于已经被移除的Frame对象之下,而新的则直接位于PageMycompany”对象之下。

        修复这个问题,可以在脚本步骤中将旧的对象替换为新的对象。

        为了保持Object Repository窗口的整洁,你应删除那些已不再存在的对象(如本例中的Frame)。

    1.9 创建Exist语句,以判断父对象的描述是否存在问题

        QTP在录制发生在某个对象上的操作时,也会同时去了解对象以及它的继承关系。因此,如果在运行过程中,QTP不能识别继承关系中的任何一个父对象,都会导致目标对象的识别失败。

        使用带有Exist方法的Msgbox语句,可以方便的检查出是继承关系中的哪个父对象出现了问题。

        假设在运行下面脚本时,你遇到了“Cannot identify Object”错误信息(但是可以确定对象仓库中Link对象的描述与当前程序中对象的描述是完全一致的):

    Browser("Yahoo!").Page("Yahoo!").Link("Arts & Humanities").Click

    你可以在该语句前面插入以下语句:

    Msgbox Browser("Yahoo!").Exist

    Msgbox Browser("Yahoo!").Page("Yahoo!").Exist

    Msgbox Browser("Yahoo!").Page("Yahoo!").Link("Arts & Humanities").Exist

    然后你从头开始运行脚本,发现第1行语句运

        但是,当QTP运行到第2步时,就弹出信息框,内容为“False”,表示不能找到Page object。这就证明Page对象存在问题。

        确定了出现问题的父对象以后,修复该问题。参见1.4

    2. 对象不唯一(The object is not unique

    在被测页面或窗口中的同一父对象下,找到多个与仓库中对象描述相符的Run-tim对象。

    请确定是只有个别对象存在此问题,还是此类对象的所有对象(或部分对象)存在此问题。

    此类对象的所有对象(或部分对象)都存在此问题吗?

    如果是,参考2.1

    如果否,参考2.2 

    2.1 为某对象类设置能唯一识别对象的描述规则(Configuring Unique Test Object Descrīptions for a Test Object Class

        如果你发现某类对象的对象描述都不足以唯一识别对象,你可以在Object Identification对话框中,改变该类的识别属性的设置,或定义该类的Smart Identification设置并启用Smart Identification机制,这样QTP就可以唯一识别对象了。

     

    2.2 设置通唯一识别对象的对象描述(Creating a Unique Test Object Descrīption for an Object

    查看当前对象的描述,并偿试在程序中找到其它具有相同描述的对象并查看它们的描述。

    >>>如果你找到了可以将它们区别开来的一个或多个属性,则修改仓库中对象的相关属性(如向描述中添加属性等),以使QTP在运行时可以唯一识别它。

    >>>如果它们的识别属性完全相同,则在对象描述中为对象添加一个ordinal identifierindexlocation)。 

    3. 找不到父对象(Parent not found

    识别对象时,会先识别它的父对象是否正确。如果遇到“Parent not found”的错误信息,请偿试以下解决方法:

    >>>修复出现问题的父对象的描述。参见1.4

    >>>在脚本中找到继承关系不正确的实例,并修改为正确的继承关系。例如1.8.1

    4. 无效的参数(Invalid arguments

    一个或多个方法的参数是无效的。

    参数无效可能是参数类型错误,也可能是参数个数不正确。

    根据以下建议,找到方法的正确语法:

    >>>Keyword模式,如果方法包括参数,则在Value栏会显示该方法的参数提示。

    >>>Expert模式,如果方法包括参数,则当你插入方法然后按下Shift+Ctrl+Space键时,QTP会显示该方法的参数的提示信息。(注:只有在Editor Options对话框的General页签中打开了Statement completion功能时才可以看到参数的提示信息。) 

    5. 不能识别itemCannot identify item

    QTP不能识别方法参数中指定的列表或树对象的item时,请确认指定的item是否存在于对象之中。

    <SPAN style="FONT-FAMILY: 宋体; FONT-SIZE: 10pt; mso-font-k

  • 自动化测试框架指南(转载)

    2009-02-02 10:41:07

    这是我以前写的一篇文章,用于整理自己对自动化测试的理解。当时写这个文章的目的,是因为刚刚掌握QTP,又使用自动化测试参与公司一个大项目的测试,结果发现原来掌握QTP距离自动化测试还有很遥远的路要走,原来一直以为掌握了QTP的脚本编写、可以写出所有的测试方法脚本则自动化测试就可以大功告成了。但是现实是残酷的,实际和自己所想的相差太远了——实际的情况是需求变化快,甚至有段时间开发还没有需求变化快,自动化测试脚本的维护工作量就可想而知了。

    因此我当时就咨询了一下其他的测试同行,他们都认为测试代码复用是很重要的问题,要搭建一个好的测试框架,这就是我当时写这篇文章的目的。

    但是在写了这篇文章后,因为工作原因没有用实践去验证文章里的思想,直到今天才有时间来温习以前的教训。今天来按实际来做时,发现了一个问题——用什么方式来划分test level service function 的颗粒呢?
    打个比方来说,我要写一个测试函数,实现以下功能:我要测试的是登录一个系统,打开一个页面,然后新建一条记录。
    因为还有其他的测试函数,肯定与这个函数有相同的代码部分,比如登录就是显而易见,但是还有一些代码肯定也是重复,而且是隐藏的,那么用什么方法把它们挖掘出来,细分的原则是什么?我实在想不清楚,需要大家的指点


    文章里的一些内容取自别人的帖子或与同行的交流,所以只能算是半原创

    自动化测试框架指南

    以下只是测试框架的一点设想,需要以后修改;
    这套方案的最终结果是实现测试自动化,但是因为目前人力、实力有限,只能逐步完善设想中的功能;最终的目的是要实现define the driver——定义驱动测试。
    本文的自动化测试以MI公司的QuickTest professional 为例
    1定义:
            Services function :业务函数
            TestCase(测试用例):是能够从头至尾独立执行的最小测试单元
            测试框架的设想

    1.1Services Function 的分类及分类原则
    Service Function的颗粒大小需求不一,靠自己来掌握,总之应该是尽量少的Service Function满足所有Case Function的需要
            Common level¬——所有项目测试都可以使用的函数,比如验证小数精度、写测试结果到报告等等。
    Common level是公用的函数库,不需要经常修改,因此可以编成DLL文件,供所有的测试脚本使用。
    使用语法可以这样:
    ‘------------------------------------
    Set ōbject=createobject(“”)
    Call object.funciton “”
    ‘------------------------------------

            High level¬——各项目专用的测试用例,是为专门的测试项目而设置的,但是这些Services Function不能单独作测试,必须配合更高一级的Test level才能使用
            Test level¬¬——Test level可以这样理解:是对某一个用户来说,为了完成某项工作和业务,时间从头至尾相对连续的一组操作。
            Test level并不是测试用例,但是它的颗粒大小却决定了其复用程度,因此需要仔细分析每个TestCase的业务逻辑,将相同的Test Level services function 总结出来。
            Test level的组成:
    Function
    Step          ‘测试所要进行的操作
    Validation     ‘验证测试的结果
    Return result   ‘返回测试的结果,validation的验证结果也应该通过这一部分的函数写入到result report中
    End function

    1.2 Test case 和Test suite
            Test Case:测试用例。可以这样理解:是一组人为了完成某项工作和业务,时间从头至尾相对连续的一组操作
            Test suite: 是一个相同工作性质的工作部门人员,为了完成某项工作和业务,时间从头至尾相对连续的一组操作。
            Test case和Test suite的意义:
    1、大量的Case,肯定是分模块存放的。否则就难以查询和维护、修改。
    2、Test Case和Test level \high level service function的互相调用关系可以通过insight sources这个工具来查询。
    3、Suite相当于一个Case模块,里面包含很多个Case;比如测试用户管理的,都放在一个Suite里,测试设备管理的,放在另一个suite里。
    1.3TestCase的分类原则
            一般复杂Case,要牵扯到好多个模块的功能的,但是要看它的主要测试点是什么,然后按这个测试点所属模块,来确定这个Case归属哪个模块的。
            有依赖关系的Case,是合并成一个Case,还是保留独立?运行起来有依赖关系,倾向于合并成一个Case,合并的好处是运行方便,但是出错时要再区分是那个小Case的错误;分开的话,就相反,运行不方便,但出错时比较明确哪个错了。
            如果A是建10万个用户,要花1小时的时间,那你还会放在一块嘛,肯定是倾向分开成小Case,不然B出错了,你还得再重头跑ABCD,测试人员会气死的!所以运行麻烦、容易出错、时间较长的小case,还是保持独立,只要跟测试人员写好说明文档,让他们知道正确的运行方法,就可以了
            如果合成一个case,我应该把它放到哪个suite里呢 因为它横跨了几个页面,都是测试点,不好划分啊。放在那个Suite里啊,那都可以啊,或者你想独立一个suite也可以啊,无所谓的,只要你运行结果有正确记录,不会漏掉丢失就可以了。
            测试环境可以通过重新导入数据来恢复,这样就可以将一部分运行时间长、但是又有依赖关系的Test case分离出来,避免总是要从开头进行测试。
            一个Test suite里的用到的lib和OR都是相同的。




    1.4测试用例和Services Function命名规则
    类型        名称
    Test case        项目名_TC_name
    test level services function        项目名_TL_name
    high level services function        项目名_HL_name
    common level services function        CL_name(不应包括项目名,因为此类函数是公用的)
    2工作方式
    并非所有的测试用例都可以用自动化来完成,因此需要对用例进行挑选,选择合适的用例作为自动化测试用例。记住!自动化测试的成本是巨大的,一般来说,一个脚本运行6~7次才算收回成本,因此不可寄予自动化测试过高期望。
    2.1选择自动化测试用例
    2.1.1不适合自动化测试用例的情况
            定制型项目(一次性的)。为客户定制的项目,维护期由客户方承担的,甚至采用的开发语言、运行环境也是客户特别要求的,即公司在这方面的测试积累就少,这样的项目不适合作自动化测试。
            项目周期很短的项目。项目周期很短,测试周期很短,就不值得花精力去投资自动化测试,好不容易建立起的测试脚本,不能得到重复的利用是不现实的。
            业务规则复杂的对象。业务规则复杂的对象,有很多的逻辑关系、运算关系,工具就很难测试。
            美观、声音、易用性测试。人的感观方面的:界面的美观、声音的体验、易用性的测试,也只有人来测试。
            测试很少运行。测试很少运行,对自动化测试就是一种浪费。自动化测试就是让它不厌其烦的、反反复复的运行才有效率。
            软件不稳定。软件不稳定,则会由于这些不稳定因素导致自动化测试失败。只有当软件达到相对的稳定,没有界面性严重错误和中断错误才能开始自动化测试。
            涉及物理交互。工具很难完成与物理设备的交互,比如刷卡的测试等。
    2.1.2适合自动化测试的情况
    自动化测试之所以能在很多大公司实施起来,就是有它适合自动化测试的特点和高的投资回报率。
            产品型项目。产品型的项目,每个项目只改进少量的功能,但每个项目必须反反复复的测试那些没有改动过的功能。这部分测试完全可以让自动化测试来承担, 同时可以把新加入的功能的测试也慢慢地加入到自动化测试当中。
            增量式开发、持续集成项目。由于这种开发模式是频繁的发布新版本进行测试,也就需要频繁的自动化测试,以便把人从中解脱出来测试新的功能。
            能够自动编译、自动发布的系统。要能够完全实现自动化测试,必须具有能够自动化编译,自动化发布系统进行测试的功能。 当然,不能达到这个要求也可以在手工干预的情况下进行自动化测试。
            回归测试。回归测试是自动化测试的强项,它能够很好的验证你是否引入了新的缺陷,老的缺陷是否修改过来了。在某种程度上可以把自动化测试工具叫做回归测试工具。
            多次重复、机械性动作,将烦琐的任务转化为自动化测试。自动化测试最适用于多次重复、机械性动作,这样的测试对它来说从不会失败。比如要向系统输入大量的相似数据来测试压力和报表。
            需要频繁运行测试。在一个项目中需要频繁的运行测试,测试周期按天算,就能最大限度的利用测试脚本,提高工作效率。
    2.2编写Test case和Test level
    分析Test Case的业务,将Test Level services function 的颗粒从Test Case中识别出来,尽量做到用少的Service function来实现测试业务。
    2.3搭建测试框架
    依据测试框架,在下一节中提到。依次填入测试框架的内容。
    2.4执行测试并记录bug
    这时就可以开始执行测试。测试结果应该自动被记录在测试报告中,而不应该一遇到BUG就停止——除非必须停止。这里注意以下几点
            测试报告功能应该在Common level中实现,这样所有的测试都可以共用。
            测试框架应该具有一定的判断功能,一旦某个测试失败。测试框架可以决定停止测试,或者转入不受影响的新测试用例,Test suite分类也应该注意这一点,因为同一个Test suite一般来说是互相影响的。
            测试框架可以具有某种还原测试环境的功能——即测试结束清理的功能,这样就可以自动恢复到不受影响的测试环境中。
    2.5维护测试脚本
    这是一项工作量很大的工作。维护脚本的难度很大程度上与团队活动有关,相关信息参考第4节。
    3测试框架的构想
    3.1Test Driver
    测试框架的核心叫Test driver,它具有以下一些东西
            全局参数。
            所要测试的用例集,也许叫Test suite集更合适;包括测试所要用到的参数。
            对于用例的描述。
            lib and tsr。
            能够判断测试结果,并且决定是否调用其它的测试用例,或者停止测试。
            自动生成测试报告。以及需要输出的路径。
            每个测试脚本的初始设置路径
    4团队开展自动化测试要点
    单人自动化测试与团队开展自动化测试有很大不同,因为不同的对象名、不同的函数会造成每个人的测试脚本不同,并难以合并成一个完整、统一的脚本。为了解决这个问题,应该注意以下几点:
            团队成员在编写脚本时应该多使用对象库,尽量少使用描述性编程。
            统一对象名称,规定网页元素对象命名的统一规定,这样才可能在合并对象库时统一。
            统一函数命名规定。
            统一函数书写格式。
            统一对同一类型操作的处理方式——应该定期举行会议,沟通各种操作的处理方法,共同提高对系统的认识水平。
    5测试配置
    测试配置应该尽量自动完成,减少工作量。
    测试配置包括如下内容:
            测试工具的配置
            测试环境,如数据、数据库结构
    6测试初始设置
    一些测试用例相互依赖,本应该把它们合成一个测试用例;但是如果单个测试用例颗粒很大,那么在回归测试或再现缺陷时就会使人发疯,并且浪费了大量的测试时间。最好最可靠的解决办法看来只有一种,那就是将颗粒大的测试用例分离出来,同时为这个测试用例预备测试初始设置——将客户端所需要的数据库结构和数据库备份,并且作为测试初始设置保存管理。
    这里的测试初始设置并非只针对自动化测试,手工测试也被包括进来。
    6.1测试初始设置的命名办法
    TE+测试用例编号
    如测试用例为TC1.2,则TE为TE1.2
    6.2测试初始设置的保存
    测试初始设置应保存在单独的文件夹内,初始设置的路径被链接到Test driver上。
  • 利用单元测试在每个层上对PHP代码进行检查发布

    2008-10-31 10:03:46

    利用单元测试在每个层上对PHP代码进行检查发布: 2008-7-24 14:53
    作者: 网络转载 | 来源: 网络转载 | 查看: 0次 | 进入软件测试时代论坛讨论
    软件测试时代


    测试驱动的开发和单元测试是确保代码在经过修改和重大调整之后依然能如我们期望的一样工作的最新方法。在本文中,您将到如何在模块、用户界面(UI)层对自己的PHP代码进行单元测试。

    现在是凌晨 3 点。我们怎样才能知道自己的代码依然在工作呢?
    Web 应用程序是 24x7 不间断运行的,因此我的程序是否还在运行这个问题会在晚上一直困扰我。单元测试已经帮我对自己的代码建立了足够的信心 —— 这样我就可以安稳地睡个好觉了。
    单元测试 是一个为代码编写测试用例并自动运行这些测试的框架。测试驱动的开发 是一种单元测试方法,其思想是应该首先编写测试程序,并验证这些测试可以发现错误,然后才开始编写需要通过这些测试的代码。当所有测试都通过时,我们开发的特性也就完成了。这些单元测试的价值是我们可以随时运行它们 —— 在签入代码之前,重大修改之后,或者部署到正在运行的系统之后都可以。
    PHP 单元测试
    对于 PHP 来说,单元测试框架是 PHPUnit2。可以使用 PEAR 命令行作为一个 PEAR 模块来安装这个系统:% pear install PHPUnit2。
    在安装这个框架之后,可以通过创建派生于 PHPUnit2_Framework_TestCase 的测试类来编写单元测试。
    模块单元测试
    我发现开始单元测试最好的地方是在应用程序的业务逻辑模块中。我使用了一个简单的例子:这是一个对两个数字进行求和的函数。为了开始测试,我们首先编写测试用例,如下所示。

    清单 1. TestAdd.php
    assertTrue( add( 1, 2 ) == 3 ); }
      function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }
    }
    ?>

    这个 TestAdd 类有两个方法,都使用了 test前缀。每个方法都定义了一个测试,这个测试可以与清单 1 一样简单,也可以十分复杂。在本例中,我们在第一个测试中只是简单地断定 1 加 2 等于 3,在第二个测试中是 1 加 1 等于 2。
    PHPUnit2 系统定义了 assertTrue() 方法,它用来测试参数中包含的条件值是否为真。然后,我们又编写了 Add.php 模块,最初让它产生错误的结果。

    清单 2. Add.php

    现在运行单元测试时,这两个测试都会失败。

    清单 3. 测试失败
    % phpunit TestAdd.php
    PHPUnit 2.2.1 by Sebastian Bergmann.
    FF
    Time: 0.0031270980834961
    There were 2 failures:
    1) test1(TestAdd)
    2) test2(TestAdd)
    FAILURES!!!
    Tests run: 2, Failures: 2, Errors: 0, Incomplete Tests: 0.

    现在我知道这两个测试都可以正常工作了。因此,可以修改 add() 函数来真正地做实际的事情了。

    现在这两个测试都可以通过了。

    清单 4. 测试通过
    % phpunit TestAdd.php
    PHPUnit 2.2.1 by Sebastian Bergmann.
    ..
    Time: 0.0023679733276367
    OK (2 tests)%
    尽管这个测试驱动开发的例子非常简单,但是我们可以从中体会到它的思想。我们首先创建了测试用例,并且有足够多的代码让这个测试运行起来,不过结果是错误的。然后我们验证测试的确是失败的,接着实现了实际的代码使这个测试能够通过。
    我发现在实现代码时我会一直不断地添加代码,直到拥有一个覆盖所有代码路径的完整测试为止。在本文的最后,您会看到有关编写什么测试和如何编写这些测试的一些建议。

    数据库测试
    在进行模块测试之后,就可以进行数据库访问测试了。数据库访问测试 带来了两个有趣的问题。首先,我们必须在每次测试之前将数据库恢复到某个已知点。其次,要注意这种恢复可能会对现有数据库造成破坏,因此我们必须对非生产数据库进行测试,或者在编写测试用例时注意不能影响现有数据库的内容。
    数据库的单元测试是从数据库开始的。为了阐述这个问题,我们需要使用下面的简单模式。
    清单 5. Schema.sql
    DROP TABLE IF EXISTS authors;
    CREATE TABLE authors (
      id MEDIUMINT NOT NULL AUTO_INCREMENT,
      name TEXT NOT NULL,
      PRIMARY KEY ( id )
    );

    清单 5 是一个 authors 表,每条记录都有一个相关的 ID。
    接下来,就可以编写测试用例了。

    清单 6. TestAuthors.php
    assertTrue( Authors::delete_all() );
      }
      function test_insert() {
         $this->assertTrue( Authors::delete_all() );
         $this->assertTrue( Authors::insert( 'Jack' ) );
      }
      function test_insert_and_get() {
         $this->assertTrue( Authors::delete_all() );
         $this->assertTrue( Authors::insert( 'Jack' ) );
         $this->assertTrue( Authors::insert( 'Joe' ) );
         $found = Authors::get_all();
         $this->assertTrue( $found != null );
         $this->assertTrue( count( $found ) == 2 );
      }
    }
    ?>

    这组测试覆盖了从表中删除作者、向表中插入作者以及在验证作者是否存在的同时插入作者等功能。这是一个累加的测试,我发现对于寻找错误来说这非常有用。观察一下哪些测试可以正常工作,而哪些测试不能正常工作,就可以快速地找出哪些地方出错了,然后就可以进一步理解它们之间的区别。
    最初产生失败的 dblib.php PHP 数据库访问代码版本如下所示。
    清单 7. dblib.php
    getMessage()); }
        return $db;
      }
      public static function delete_all()
      {
        return false;
      }
      public static function insert( $name )
      {
        return false;
      }
      public static function get_all()
      {
        return null;
      }
    }
    ?>
    对清单 8 中的代码执行单元测试会显示这 3 个测试全部失败了:
    清单 8. dblib.php
    % phpunit TestAuthors.php
    PHPUnit 2.2.1 by Sebastian Bergmann.
    FFF
    Time: 0.007500171661377
    There were 3 failures:
    1) test_delete_all(TestAuthors)
    2) test_insert(TestAuthors)
    3) test_insert_and_get(TestAuthors)
    FAILURES!!!
    Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0.
    %

    现在我们可以开始添加正确访问数据库的代码 —— 一个方法一个方法地添加 —— 直到所有这 3 个测试都可以通过。最终版本的 dblib.php 代码如下所示。
    清单 9. 完整的 dblib.php
    getMessage()); }
        return $db;
      }
      public static function delete_all()
      {
        $db = Authors::get_db();
        $sth = $db->prepare( 'DELETE FROM authors' );
        $db->execute( $sth );
        return true;
      }
      public static function insert( $name )
      {
        $db = Authors::get_db();
        $sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' );
        $db->execute( $sth, array( $name ) );
        return true;
      }
      public static function get_all()
      {
        $db = Authors::get_db();
        $res = $db->query( "SELECT * FROM authors" );
        $rows = array();
        while( $res->fetchInto( $row ) ) { $rows []= $row; }
        return $rows;
      }
    }
    ?>

    在对这段代码运行测试时,所有的测试都可以没有问题地运行,这样我们就可以知道自己的代码可以正确工作了。

    HTML测试

    对整个 PHP 应用程序进行测试的下一个步骤是对前端的超文本标记语言(HTML)界面进行测试。要进行这种测试,我们需要一个如下所示的 Web 页面。
    这个页面对两个数字进行求和。为了对这个页面进行测试,我们首先从单元测试代码开始入手。
    清单 10. TestPage.php
    get( $url );
        $resp = $client->currentResponse();
        return $resp['body'];
      }
      function test_get()
      {
        $page = TestPage::get_page( 'http://localhost/unit/add.php' );
        $this->assertTrue( strlen( $page ) > 0 );
        $this->assertTrue( preg_match( '//', $page ) == 1 );
      }
      function test_add()
      {
        $page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' );
        $this->assertTrue( strlen( $page ) > 0 );
        $this->assertTrue( preg_match( '//', $page ) == 1 );
        preg_match( '/(.*?)/', $page, $out );
        $this->assertTrue( $out[1]=='30' );
      }
    }
    ?>

    这个测试使用了 PEAR 提供的 HTTP Client 模块。我发现它比内嵌的 PHP Client URL Library(CURL)更简单一点儿,不过也可以使用后者。
    有一个测试会检查所返回的页面,并判断这个页面是否包含 HTML。第二个测试会通过将值放到请求的 URL 中来请求计算 10 和 20 的和,然后检查返回的页面中的结果。
    这个页面的代码如下所示。

    清单 11. TestPage.php
    " /> +
    " /> =

    这个页面相当简单。两个输入域显示了请求中提供的当前值。结果 span 显示了这两个值的和。 标记标出了所有区别:它对于用户来说是不可见的,但是对于单元测试来说却是可见的。因此单元测试并不需要复杂的逻辑来找到这个值。相反,它会检索一个特定  标记的值。这样当界面发生变化时,只要 span 存在,测试就可以通过。
    与前面一样,首先编写测试用例,然后创建一个失败版本的页面。我们对失败情况进行测试,然后修改页面的内容使其可以工作。结果如下:
    清单 12. 测试失败情况,然后修改页面
    % phpunit TestPage.php
    PHPUnit 2.2.1 by Sebastian Bergmann.
    ..
    Time: 0.25711488723755
    OK (2 tests)
    %

    这两个测试都可以通过,这就意味着测试代码可以正常工作。
    不过对 HTML 前端的测试有一个缺陷:Javascrīpt。超文本传输协议(HTTP)客户机代码对页面进行检索,但是却没有执行 Javascrīpt。因此如果我们在 Javascrīpt 中有很多代码,就必须创建用户代理级的单元测试。我发现实现这种功能的最佳方法是使用 Microsoft? Internet Explorer? 内嵌的自动化层功能。通过使用 PHP 编写的 Microsoft Windows? 脚本,可以使用组件对象模型(COM)接口来控制 Internet Explorer,让它在页面之间进行导航,然后使用文档对象模型(DOM)方法在执行特定用户操作之后查找页面中的元素。


    这是我了解的对前端 Javascrīpt 代码进行单元测试的惟一一种方法。我承认它并不容易编写和维护,这些测试即使在对页面稍微进行改动时也很容易遭到破坏。
    编写哪些测试以及如何编写这些测试
    在编写测试时,我喜欢覆盖以下情况:
    所有正面测试
    这组测试可以确保所有的东西都如我们期望的一样工作。
    所有负面测试
    逐一使用这些测试,从而确保每个失效或异常情况都被测试到了。
    正面序列测试
    这组测试可以确保按照正确顺序的调用可以像我们期望的一样工作。
    负面序列测试
    这组测试可以确保当不按正确顺序进行调用时就会失败。
    负载测试
    在适当情况下,可以执行一小组测试来确定这些测试的性能在我们期望的范围之内。例如,2,000 次调用应该在 2 秒之内完成。
    资源测试
    这些测试确保应用编程接口(API)可以正确地分配并释放资源 —— 例如,连续几次调用打开、写入以及关闭基于文件的 API,从而确保没有文件依然是被打开的。
    回调测试
    对于具有回调方法的 API 来说,这些测试可以确保如果没有定义回调函数,代码可以正常运行。另外,这些测试还可以确保在定义了回调函数但是这些回调函数操作有误或产生异常时,代码依然可以正常运行。
    这是有关单元测试的几点想法。有关如何编写单元测试,我也有几点建议:

    1.不要使用随机数据

    尽管在一个界面中产生随机数据看起来貌似一个好主意,但是我们要避免这样做,因为这些数据会变得非常难以调试。如果数据是在每次调用时随机生成的,那么就可能产生一次测试时出现了错误而另外一次测试却没有出现错误的情况。如果测试需要随机数据,可以在一个文件中生成这些数据,然后每次运行时都使用这个文件。采用这种方法,我们就获得了一些 “噪音” 数据,但是仍然可以对错误进行调试。

    2.分组测试

    我们很容易累积起数千个测试,需要几个小时才能执行完。这没什么问题,但是对这些测试进行分组使我们可以快速运行某组测试并对主要关注的问题进行检查,然后晚上运行完整的测试。

    3.编写稳健的 API 和稳健的测试

    编写 API 和测试时要注意它们不能在增加新功能或修改现有功能时很容易就会崩溃,这一点非常重要。这里没有通用的绝招,但是有一条准则是那些 “振荡的” 测试(一会儿失败,一会儿成功,反复不停的测试)应该很快地丢弃。

    结束语
    单元测试对于工程师来说意义重大。它们是敏捷开发过程(这个过程非常强调编码的作用,因为文档需要一些证据证明代码是按照规范进行工作的)的一个基础。单元测试就提供了这种证据。这个过程从单元测试开始入手,这定义了代码应该 实现但目前尚未 实现的功能。因此,所有的测试最初都会失败。然后当代码接近完成时,测试就通过了。当所有测试全部通过时,代码也就变得非常完善了。
    我从来没有在不使用单元测试的情况下编写大型代码或修改大型或复杂的代码块。我通常都是在修改代码之前就为现有代码编写了单元测试,这样可以确保自己清楚在修改代码时破坏了什么(或者没有破坏什么)。这为我对自己提供给客户的代码提供了很大的信心,相信它们正在正确运行 —— 即便是在凌晨 3 点。

Open Toolbar