41
Different QTP Page 1 Different QTP [email protected] 2012-09-07

Different QTP.v3

Embed Size (px)

Citation preview

Page 1: Different QTP.v3

Different QTP

Page 1

Different QTP

[email protected]

2012-09-07

Page 2: Different QTP.v3

Different QTP

Page 2

Different QTP

前言

为什么叫做“Different QTP”, 具体来说, 在我的项目里, 下面一些是不使用的

1. Object Repository(尽量不用)

2. Reusable Action(严禁使用)

3. Smart identification(严禁使用)

4. Keyword view(没有必要用)

5. Reporter ReportEvent(尽量不用)

简单来说, QTP 里一些提高易用性的工具, 基本上统统都用不上。

QTP(QuickTestPro)作为最流行的商业软件测试工具,像其他商业软件一样,在软件的易用性

方面都是费尽心机。 设计了很多工具来让用户能够快速开始设计一个测试脚本。 但是, 最近

我在对这两年 QTP 测试工作进行总结时, 发现在我的项目里, 并没有使用 QTP 提供的这些

提高“易用性”的工具, 甚至很多工具是严禁使用的。 可以说, 我做 QTP 测试项目的开发

方式和 QTP 的推荐方式完全不一样。 这就是我把文章命名为“Different QTP“的原因。

在我看来, QTP 提供的很多工具只是为了 Quick, 比如 record, 对于小项目尚能接受, 但

对于比较大且长期运行的项目, 却带来了维护的问题, 反而是效率低下的。

也许你觉得这样比较“激进”, 但我的实践证明了这些措施是有效的。 在两年的时间里,我

按照这样的理念设计了 QTP 测试的 framework, 在此 framework 基础上 有超过 1000 的

testcase 也是在使用同样的理念开发出来。整个 Team 都觉得这些 testcase 的开发效率和可

维护性远远超过了按照常规的 QTP 方式开发的 testcase。这也是我有信心把这个过程中的经

验教训共享出来的原因。

一个 QTP 测试项目如果简单的划分, 包括三个部分。 如下所示。

Testcase

+-------------------------+ +--------+

| | | |

| TestCases | | |

| | | Mngt. |

| | | Tools |

| | | |

+-------------------------> | |

| | | |

| | | |

| | | |

| Function Library | | |

| | | |

| | | |

+-------------------------+ +--------+

Page 3: Different QTP.v3

Different QTP

Page 3

就是根据手动测试的 Testcase 实现的 QTP testcase。

Mngt. Tools

是一些脚本工具, 来自动化批量执行, 产生测试报告, 生产代码文档等工作。

Function Library

是对产品操作的封装库。 对产品的操作既包括高层次的业务层面的操作比如“创建一个用

户”, 也包括低层次的对某个 GUI 元素的操作, 比如“Click Button”。它的封装形式并不

限于 Function, QTP 支持的 Reusable Action 也是一种封装形式。一个测试框架 应该包括

Function Library 和 Mngt Tools 两个部分,但因为 Function Library 是主要的部分, 所以, 有

时候也把 Function Library 叫做 Framework。在本文里, 在不涉及 Mngt. Tools 的场合, 所以

两个名词是可以互换的。

决定一个 framework 最否有效解决问题的关键是它对问题领域的抽象。 所以下一篇, 我要介

绍 Framework 里最重要的两个抽象。

注 1:通常来说, 一个 framework 几乎都是产品特定的。 不可能完全把测试 A 产品的

framework 用在 B 产品上。 但是 framwork 的设计思路是可以共享的。 下面的系列文章里我

会具体介绍这些设计思路, 希望能对你能有所启发, 能引起你的一些思考。 任何你的 idea

也欢迎和我讨论。

Page 4: Different QTP.v3

Different QTP

Page 4

两个抽象之一:产品业务逻辑抽象

第一个抽象层是关于对产品业务逻辑的封装。 它的目标是,在设计 testcase script 时, 要求

做到“testcase script 和 testcase 的描述有简单而直接的对应”。 也就是说在阅读一个

testcase script 的时候, 加上少量的注释, 就像在阅读这个 testcase 的人类语言描述。

例子

我们来看一个 testcase 的描述。 它关注于对测试逻辑的描述, 它的语句表达了业务层面上有

意义的步骤, 而 不会/很少 涉及到很具体的 GUI 的操作, 比如下面的例子。 这是一个测试创

建新用户的 testcase 描述。

Preconditions:

Login as Administrator

Steps:

1. goto admin->user management

2. create a new user with

1) name: ABC

2) password: 123

3. logout administrator

4. login as ABC with initial password 123 and change password to 234

5. Logout ABC

6. Login as ABC and with password 234

Checkpoints

step2, ABC is created

step6, ABC can login

一个 testcase 的 script, 应该像下面的样子

'precondition

BIZ_LoginAsAdmin

'create user ABC

BIZ_CreateUser "name=ABC;password=123"

AssertTrue BIZ_UserExist "ABC", "user ABC not created"

BIZ_Logout

'login as ABC and change pswd

BIZ_LoginWithInitPswd "ABC", "123", "234"

BIZ_Logout

'login as ABC again

loginFlag = BIZ_Login "ABC", "234"

AssertTrue loginFlag, "ABC failed to login with pswd 234"

Page 5: Different QTP.v3

Different QTP

Page 5

如你所见, 一个 testcase script 的开发人与, 在编写 testcase script 的时候, 同样也只关注

测试逻辑, 他用 framework 提供的 API 来把人类语言的测试逻辑“翻译”为 VBS 语言。 这

就是第一个抽象/封装所要达到的目标。

优点与缺点

这种方法的最大优点就是。 testcase script 的开发效率更高, 同时维护成本更低。

首先是项目代码更能适应变化了, 因为他对两个变化源彻底的分离, 无论是产品功能的变化,

或者测试逻辑的变化, 其影响范围都限制在了最小。

testcase script 的简单易读使得后期维护更容易。 设想你去维护一个别人开发的 QTP project。

当你打开一个失败的 testcase 时, 你能够通过读代码就能明了其测试逻辑, 马上开始调试工

作。。。

Testcas script 的开发效率更高。在对产品功能有一个全面而系统的封装的天体下, Testcase

开发的要求降低了,无论是复杂度还是代码量都会少很多。 但是, 这需要更多的时间来开发

framwork, 所以总时间并不一定会少。 但我能确定后期维护时间会少很多。

这样做的缺点也有, 就是对 framework 开发人员要求更高,除了会使用 QTP, 还 必须具有

良好分析和抽象的能力, 我认为至少要有一年的通用的面向对象语言开发经验

(Java/C#/C++)。 公司需要更高的价格才能在市场上雇佣到这样的人。

在实践的过程中,我发现对于 framework 的开发者的要求,首先要非常熟悉业务, 第二就是

具有良好的分析和抽象的能力。即使你满足了这两个要求, 还需要时刻把下面一个原则记在

心里。

Page 6: Different QTP.v3

Different QTP

Page 6

业务逻辑封装原则

业务逻辑封装原则就是 严格分离测试逻辑和业务逻辑

这里的测试逻辑就是指 Testcase 里面的逻辑, 业务逻辑就是产品功能相关的逻辑。

这个原则看上去很简单明了, 但是实践时非常容易把二者混淆。

为了“严格分离测试逻辑和业务逻辑”, 我对开发人员的角色进行了划分, 分为“framwork

开发人员”和“testcase script 开发人员”。 framework 开发人员在开发封装业务逻辑的 API

的时候, 心里要忘掉任何具体的某个 testcase 的测试逻辑, 而只关心如何能从产品业务逻辑

的角度提供最合适的 API。

其次, 我还对函数所在的位置进行了隔离。 所有业务逻辑封装的代码放在函数库目录里, 而

与测试逻辑相关的一些共享函数放在 TestCase repository 目录里。

即便如此, 开发人员也会遇到“不能确定一个 function 是属于业务逻辑还是测试逻辑的问

题”。 我举了两个个例子。

验证用户存在

验证用户存在是一个经常会用到的功能, 所以对于开发人员有极大的诱惑把它封装为一个函

数, 放在 framework 里。 但实际上这是错误的, 因为“验证”这个动作本身就与业务逻辑无

关, 而属于测试逻辑。

创建一组特定的 “角色,用户, 用户组”

在某件些 testcase 的前置条件里, 要求一组特定的“角色,用户,用户组”存在。 写一个函

数封装这一系列操作是一个好主意, 但这个函数仍然属于测试逻辑。 因为对于产品来说, 并

没有一组“角色,用户, 用户组”这个概念。

Page 7: Different QTP.v3

Different QTP

Page 7

业务逻辑封装接口设计

为了达到“testcase script 和 testcase 的描述有简单而直接的对应”这样一个目标, 需要对

产品功能有一个全面而系统的封装, 然后提供一套 API 供 testcase script 调用。 显然, 这会

导致大量的 API 单元产生出来。

QTP 提供了 resuable action 作为业务逻辑封装的工具, 但是 resuable action 过于重型, 而

且并没有提供与其重量级相对应的强大功能。 function 相比于 reusable action 提供了同样的

封装能力, 但体积只有 1/10,而且使用方式更自然。 这也是不用 reuable action 的若干重要

原因之一。

另外一个必须要考虑的问题就是如何降低这套 API 使用的难度。 这里有一些技巧。

命名规则

VBS 没有 Module 的概念, 但显然数量众多业务逻辑封装函数必须组织在模块里。 所以我们

在命名规则上来模块化。一个业务封装函数的命名如下

BIZ_ModuleName_DoWhat(paramters)

首先 BIZ 前缀把它与 framework 里的其他函数分离出来。 ModuleName 是第二个前缀,

指出了模块名。 第三个参数才描述了函数所封装的业务。

文档化

VBS 没有专门的文档化定义(类似 JavaDoc 的东东)。当然你不能对它责怪太多, 毕竟这

是上个世纪 70 年代的语言。 我使用 Doxygen 加上一个定制的 filter 来生成 framwork

里所有函数的文档。

比如下面这段的代码

' create account with given parameters.

'

' @param resourceType Hierarchy/Group/Device

' @param param1 the param to set value in page 1

' @param param2 the param to set value in page 2

'

' @see GUI_Field_HierarchySelect_Set

' @see GUI_Field_GroupSelect_Set

' @see GUI_Field_ProfileSelect_Set

Function BIZ_Account_Create(accountName, roleName, resourceType,

param1, param2)

通过 Doxygen 生成的文档像下面的样子

Page 8: Different QTP.v3

Different QTP

Page 8

去除页面依赖

这里的“页面依赖“是一种函数前置条件。 它特指业务逻辑封装函数对进入此函数时的当前

页面的要求。比如一个 BIZ_User_Create 函数, 它的前置条件可以是“当前页面在 User

Management 页面”。 不满足前置条件时, 调用一个函数通常会导致 object not

found 错误。

但是如果 BIZ_User_Create 函数针真的对前置条件如此要求, 那么成百上千的类似的业

务封装函数会让 Testcase script 的设计者“步步惊心”。 为了消除这种不必要的心理

负担。 我对业务逻辑封装函数的要求就是:只要有可能, 所有业务逻辑函数都要去除页面

依赖。也就是说无论当前应用程序处于那一个页面, 函数都能正常工作。对于大部分应用程

序来说,前置条件的要求其实是“用户已经登陆进入系统, 但可以处于任意页面”。

显然, 这会导致更多的执行时间。在函数内部, 首先做的就是导航(通过菜单或别的方式)

到实际开始业务逻辑的页面。 但是相对于其带来的接口的简化, 和后续的维护简化。 这点

成本简直就是微不足道。

这是一个很小的规则, 但它在提高 API 的可用性方面效果非常大。

Page 9: Different QTP.v3

Different QTP

Page 9

两个抽象之二:GUI 元素操作抽象层

第二个抽象层是关于对 GUI 元素的操作的封装。

在我的项目里,除了最顶层的 Window, 几乎完全不使用 Object Repository 来定义 GUI 元

素。而是使用一套对 GUI 元素封装的 API。

对于 GUI 元素的封装的 API。 它的目标是, 在编码的时候, 对 GUI 元素的操作是“所见即

所得”。 什么叫做“所见即所得”, 它的意思是, 当访问一个对象时, 通过你能看见的标

识就能得到这个对象。

这个的关键是“你能看见的标识”。 如果你用过 QTP 的 record, 你得到代码里,对于 GUI

对象的访问通常都是通过“看不见的标识”, 比如 HTML ID, index 等等。

例子

比如下面的一个 login web 页面。

对于 login 操作, 如果通过 record,代码像下面的样子

Browser("XXX").Page("YYY").WebEdit("loginform:name").Set "ABC"

Browser("XXX").Page("YYY").WebEdit("loginform:pswd").Set "111111"

Browser("XXX").Page("YYY").WebButton("loginform:login").Click

而使用“所见即所得”的 GUI 元素库, 代码像下面的样子

GEdit("Login name").Set "ABC"

GEdit("Password").Set "111111"

GButton("Login").Click

如你所见, 对元素的引用通过你能看见的标识,而不是 id 或者 name 你不能看见的标识。这

也是不使用 Object Repository 的重要原因之一。

优点与缺点

这样一种抽象/封装方式带来的好处,首先是开发效率的提高。 在写代码的时候, 完全没有了

Object Repository 的束缚, 不用再在 editor 和 object repository manager 之间来回切换,坦

白说, 那种开发方式几乎让我疯掉。 你所做的就像在编写任何其他语言代码一样,以一种最

舒服的方式在开发你的 function 或者 testcase script。

另外是在对代码修改以适应产品 GUI 的变化时, 你仍旧不需要打开 inspector 和 object

repository manager, 去重新识别元素。 你是需要把你看到的变化在代码里重新匹配即可。

Page 10: Different QTP.v3

Different QTP

Page 10

缺点。 对某一个特定的产品, 建立一套完整的 GUI 对象查找机制并不是一个很小的工作量。

而且在这个过程中还会经常遇到“非常规”需要特别处理的 GUI 元素。 为了达到易用性和能

力的平衡, 需要做出很多设计上的取舍, 这需要一个资深的程序设计人员才能完成。 如果非

要量化的话, 我认为其设计者需要三年通用的面向对象语言开发经验(Java/C#/C++)。

GUI 元素库是 framwork 里最重要的部分, 也是工作量最大的部分。 它也是业务逻辑库的基

础。 所以我会用很多笔墨来详细介绍它是设计的, 和设计过程中面对的各种问题和解决方法。

Page 11: Different QTP.v3

Different QTP

Page 11

GUI 元素库:架构

GUI 元素库的总体结构如下图所示。

|----------------------------------------|

| Shortcut |

|----------------------------------------|

|FormInput | SmartInput | CombinedElement|

|----------------------------------------|

| GUIElementQuery |

|----------------------------------------|

其中,

GUIElementQuery

这是一些提供最底层的 GUI 元素查找函数。比如针对 ID 进行查找, 针对“Attached Text”

进行查找。

CombinedElement

对组合元素的封装。 一个软件产品, 总会有一些组合的输入控件, 比如“日期”输入控件可

能由“年月日”三个 Edit Box 组合而成。

SmartInput

SmartInput 提供智能查找支持。也就是在查找 GUI 元素时, 不需要指定其类型。 SmartInput

会按照给定的类型顺序, 逐个尝试。 SmartInput 是 FormInput 的基础。

FormInput

FormInput 提供对一个 Form 里的多个 Input 元素进行批量的读取或者设置值的操作。

FormInput 把测试数据和测试逻辑分离开来, 提高代码开发的效率。 而且 建立在 SmartInput

之上的 FormInput 对于大多数基本 GUI 元素无须指定类型, 这样可以仅仅把“名字:值”传

递给 SmartInput 就可以对一个 Form 进行设置值的操作, 进一步减轻开发的负担, 提高开发

效率。

Shortcut

Shortcut 封装了底层最常用的方法, 并提供一套快捷访问接口。 比如前面例子中

GEdit("Login Name")就是一个快捷方式。 它实际上会调用到下面的代码。

GUI_GetElementByLabel(Browser("XXX").Page("YYYY"), "Login Name",

"WebEdit")

可见, GUI 元素库不但提供了取代 Object Repository 的功能, 还能进行批量数据输

入, 而且通过其 shortcut 接口, 还提供了更高效的代码书写风格。

Page 12: Different QTP.v3

Different QTP

Page 12

GUI 元素库:查找函数

QTP 对于 GUI 对象查找的能力是相当之强的。 这个通过 Object Repository 里属性设置

就可以看出来。 对于某一个特定的软件产品来说, 其实并不需要所有的这些能力。 设计

GUI 元素查找函数就是分析所有这个软件产品涉及到的 GUI 元素, 把他们分门别类, 针对

每种类型设计相应的查找函数。

GUI 元素查找函数底层都是使用描述性编程(Descriptive Programming)来查找 GUI

元素的。

我们通过一个实例来详细介绍这个 GUI 元素查找函数库。

查找函数

一个真实的对 Web Application 测试的 QTP project,有下面一些查找函数

GUI_Html_FindByTextAndTag(objContainer, strText, strTag, xtype)

这个函数具有根据“inner text”和“tag”来查找对象的能力

GUI_Html_FindById(objContainer, strId, xtype)

这个函数具有根据 ID 属性来查找对象的能力

GUI_Html_FindByName(objContainer, strName, xtype)

这个函数具有根据 Name 属性来查找对象的能力

GUI_Html_FindFromObjRepo(objContainer, strName, xtype)

这个函数具有从 Object Repository 查找对象的能力。 存在极少的一些 element, 把它放到

Object Repository 里可能是更有效率的方式。

GUI_Html_FindByLabel(objContainer, label, xtype)

这个函数具有根据 Label 来查找 Input 元素的能力。 关于什么是 label, 请参考 web 方面的文

档。 前面例子的 GEdit("Login Name"), 就是根据 Label 来查找 Input 元素的实例。

GUI_Html_FindTableByColumns(objContainer, strColNames)

这个函数具有根据 column name 来查找 Table 的能力。 实践表明,Table 是一个经常会

用到的 GUI Element, 而 Column name 是最能标识一个 Table 的(所见即所得风格)。

入口函数

作为一个封装良好的 Library, 并不需要把这么多接口统统暴露出来。 实际上, 这里还

提供了一个总入口作为对外的接口。

GUI_Html_FindElement(objContainer, strMark, xtypye)

Page 13: Different QTP.v3

Different QTP

Page 13

这个函数会根据 xtype 之不同, 按照特定顺序调用上面的函数来查找一个 GUI element,

直到找到为止, 或者返回 Nothing 表示没有找到。

作为设计者, 你必须要明确每一种 Element 可能具有的查找方式, 比如

link:id,name,textandtag, objrepo

webedit:id,name,label,objrepo

webtable:id, columns, objrepo

总的来说。 外部程序只需要调用查找函数库的入口函数就可以完成绝大多数 GUI element 的

查找。

Find 与 Get

如果你使用 Object Repository 来定位 GUI 元素,QTP 提供了一个很好的机制来提高代码的稳

定性, 它就是一个等待对象出现的机制。 它的原理是这样,比如这样一行代码:

Browser("XXX").Page("YYY").WebEdit("loginform:name").Set "ABC"

如果执行到这里的时候,WebEdit("loginform:name")还未出现(比如 page 还没有完全

load), 那么 QTP 会等待一段时间。 只要在这个时间范围内, 此元素在页面生成, 就

会顺利执行下去, 而没有任何类似对象不存在的错误抛出。

但是对于 GUI 对象查找函数, 这种机制是无效的, 因为它使用 Descriptive

Programming。 所以有必要提供类似机制。 在 GUI 元素查找库里提供了下面一个接口:

GUI_GetElement(objContainer, strMark, xtypye)

GUI_GetElement 相对于与 GUI_FindElement 之不同在于, 前者有一个 timeout 时间,

如果在 timeout 时间之内没有找到 GUI element, 会抛出一个 Error;而后者是马上返

回, 如果没有找到, 返回 Nothing。 GUI_GetElement 的实现像下面的样子

GUI_GETELEMENT_TIMEOUT=20 'seconds

Function GUI_GetElement(objContainer, strMark, xtype)

Set ele = Nothing

for idx=0 to GUI_GETELEMEN_TIMEOUT

Set ele = GUI_FindElement(objContainer, strMark, xtype)

if Not ele Is Nothing Then

Exit For

end if

Wait 1 'sleep 1 second

next

AssertNotNothing ele, "Failed to find object with

mark<"&strMark&">, xtype<"&xtype&">"

End Function

Page 14: Different QTP.v3

Different QTP

Page 14

Index 标识

有时候在一个界面上, 两个 Element 具有相同的标识。 比如下面的例子, 开始时间和结束时

间都具有 Day, Hour 和 Minute 三个控件。

对于这种情况,就需要指定 Input 控件的序号, 也就是所谓的“Index”。 默认 index 是 1,

无需指定。 如果 index 不是 1,比如 2, 那么用“#2“来标识。比如得到第二个“Day”控件。

Set objEndDay = GUI_FindElement(objContainer, "Day#2", "JavaSpin")

使用 Index 标识带来一个问题, 就是“#”成了特殊字符。 如果你的标识符里碰巧有#, 而且

碰巧#后面是数字, 那么需要用\来转义。 其他情况都不需要转义,程序会把#解释为字面量而

不是 Index 标识符。

额外说明

为什么提供在 Object repository 里查找 element 的函数,这不是不符合”所见即所得“吗?

这是因为, 查找函数库要提供最大的元素查找能力, 并不代表推荐这样的查找方式。 只要尽

可能, 首选“可见即所得”的方式, 比如对于一个 table, 首先选择 column names 而不是

ID。

好难达到“所见即所得”

在某些时候需要你发挥自己的想象力以获得“所见即所得”的能力。 比如, 如果 Form 里的

Input 并没有对应 Label, 但是所有 input 放在具有两个 column 的 table 里, 第一个 column

是 input 的描述, 第二个 column 里 input 本身, 那么你可以设计一个

GUI_FindInputInTable(objTableContainer, strColName, xtype) 这样一个函

数, 同样达到“所见即所得”的效果。

Page 15: Different QTP.v3

Different QTP

Page 15

GUI 元素库:标识符匹配的规则

GUI 元素查找函数库提供了一个入口函数,

GUI_Html_FindElement(objContainer, strMark, xtypye)

这个函数的参数 strMark 是一个字符串, 在查找时用来匹配元素的某一个属性。 说到“匹

配”, 我们马上会想到“相等”, 这是最常用的一种匹配。如果为了提供更大的灵活性,

还应该支持“正则表达式”匹配。 实践表明, 在“相等”和“正则表达式”之间, 还有一

种常用的匹配关系“包含”。”包含“关系匹配的好处是你可以只指定关键字, 而又不需要

写稍微复杂的正则表达式。

比如你想匹配一个页面抛出的错误信息.

<div class="Error">Error: invalid user name. user name shouldn't

contain special characters like "#/' ...</div>

对你来说, 你其实只是想确定页面错误信息里有“invalid user name”就可以了, 那

么这里就会用到“包含”关系的匹配。

Set objErrDiv = GUI_Html_FindElement(objContainer, "<DIV>%invalid

username%", "WebElement")

稍微解释一下 strMark。 因为这里 xtype 指定的是 WebElement, 对于这种类型, 底

层查找函数是 GUI_Html_FindByTextAndTag(objContainer, strText, strTag,

xtype)。 但是这里有一个问题是, 如何通过 strMark 一个字符串来表达 strText 和

strTag 两个内容。 这里就设计了"<tagName>innerText"这样一种表达形式。

为了支持以上三种匹配关系, 而且代码书写方式还要自然, 高效。 我定义了下面的规则

如果 strMark 是“/.../”的形式,那么表示用正则表达式匹配

如果 strMark 是“%...%“的形式, 那么表示用“包含”匹配

如果 strMark 不是上面两种形式之一, 那么表示用”相等“匹配

通过匹配规则的扩展, 查找函数库的调用者获得了更大的查找对象的能力, 同时几乎没有增

加额外的负担。

额外说明

有些查找方法不支持正则表达式匹配或者”包含“匹配, 比如从 object repository 查找元素。

这些限制应该在接口文档里明确的指出。

Page 16: Different QTP.v3

Different QTP

Page 16

GUI 元素库:智能查找与批量输入

SmartInput

SmartInput 提供了无需指定元素类型就能查找 Input 元素的能力。 还是以前面的 login 页面作

为例子。

如果要得到 Login name 对应的 edit box。 代码如下

Set obj = GUI_Html_GetElement(objContainer, "Login name", "WebEdit")

如果使用 SmartInput, 代码如下

Set obj = GUI_SmartInput_Get(objContainer, "Login name")

SmartInput 会按照下面的顺序依次查找 element。如果最终没有找到,那么返回

Nothing。

"WebEdit", "WebList", "WebCheckBox", "WebRadioGroup", "WebFile"

对于单独的一个 Input, 看上去意义不大。 但是如果你面对一个具有几十个 Input 的 Form,

那么结合 FormInput, 在提高开发效率方面就有可观效果。

FormInput

FormInput 提供批量读取/设置 Form 里 input 的值的方法。

对于企业应用程序, 常常会有处理包含几十个 Input 的 Form 的情况。如下例所示, 这个界

面包含 16 个 Input 控件, 而且这还只是一个 Wizrd 的 5 个界面之一。

Page 17: Different QTP.v3

Different QTP

Page 17

基本形式

对于这种情况, 批量输入数据是很有意义的。 比如设计下面一个接口。

GUI_FormInput(objContainer, nameValueMap, nameTypeMap)

调用代码如下

GUI_FormInput(objWizard, _

"Activated=True; Name=xs; Schedule=Monthly; Day=31; Hour=2;

Minute=32;Day#2=31; Hour#2=2; Minute#2=32",

"Activated=JavaCheckBox; Name=JavaEdit; Schedule=JavaList;

Day=JavaSpin; Hour=JavaSpin; Minute=JavaSpin; Day#2=JavaSpin;

Hour#2=JavaSpin; Minute#2=JavaSpin")

相对于每一个 Input 控件逐个调用相应的查找函数, FormInput 节省了的大量的代码。

利用 SmartInput

GUI_FormInput(objWizard, _

"Activated=True; Name=xs; Schedule=Monthly; Day=31; Hour=2;

Minute=32;Day#2=31; Hour#2=2; Minute#2=32",

"")

可以看到,第三个参数为空字符串,也就是不用指定 Input 的类型。 因为所有的 Input 控

件都被 SmartInput 支持。

返回已有值

另外, 调用 GUI_FormInput 函数的时候, 返回 Input 的值在某些情况下也是很有用的功能。

实际上对于实现来说, 在设置 Input 的值时, 获取 Input 已有的值是很容易的。 所以

FormInput 还会返回 Form 里所有 Input 的值, 如下所示。

Set mapFormVal = GUI_FormInput(objWizard, _

"Activated=True; Name=xs; Schedule=Monthly; Day=31; Hour=2;

Minute=32;Day#2=31; Hour#2=2; Minute#2=32",

"")

Page 18: Different QTP.v3

Different QTP

Page 18

GUI 元素库:复杂对象描述机制

在介绍 FormInput 的时候,在传递 Form 里要设置的数据时, 使用了下面一个字符串。

"Activated=True; Name=xs; Schedule=Monthly; Day=31; Hour=2;

Minute=32;Day#2=31; Hour#2=2; Minute#2=32"

直觉告诉你这代表了一个 Map。 没错,这里用到了 framework 里相当重要的一个基础技术,

那就是复杂对象的描述技术。

对于 QTP 测试项目来说, 传递复杂的数据结构是经常会碰到的情况。但是无论 VBS 还是

QTP 都没有提供一种方便的解决方案。

本质上, 我们需要一个像 JSON 一样, 用字符串描述复杂数据结构的技术。 借鉴 JSON,

我定义了下面的语法规则。它甚至比 JSON 更简单, 因为它只有一种数据类型“String”。

Page 19: Different QTP.v3

Different QTP

Page 19

map :

{}

{members}

members :

pair

pair; members

pair :

NULL # NULL means nothing. NULL will be ignored, e.g

"a=1;;b=2"

string = value #string will be trimmed

array :

[]

[elements]

elements:

NULL # NULL means nothing. NULL will be ignored, e.g

"abc,,def"

value

value, elements

value :

NULL # ""

string

map

ary

string :

char chars

char

normal # any char but not \ [ ] { } ; , =

\[

\]

\{

\}

\=

\;

\,

\\

\s # (32)

\t # (9)

\n # (10)

\r # (13)

Page 20: Different QTP.v3

Different QTP

Page 20

使用这个语法, 能描述 Map 和 Array, 和他们的任意的组合。例子如下

map 的例子是 "{name=Jack; age=30}"

array 的例子是 "[Jack, Tom]"

组合的例子是 [{name=Jack; age=30}, {name=Tom; age=20}]

特殊字符用'\'来转义, 例子是 “{name=Jack\, Tom; age=30}”

复杂对象描述机制极大的提高了代码生产率, 并且让你的代码看上去很美。 如果没有它, 我

一定会不去做任何 QTP 的项目,因为我忍受不了如此丑陋的代码。 想象一下用 Dictionary 逐

个赋值的方式来构造输入数据。。。

Page 21: Different QTP.v3

Different QTP

Page 21

GUI 元素库:组合元素

前面介绍的都是 QTP 提供的基本元素的查找和操作。 对于一个软件产品,经常会看到这样一

种情况, 页面上的几个基本输入元素在逻辑上是一个输入元素。比如下面的例子。

三个基本输入元素, 一个 WebEdit, 两个 WebList 构成了一个逻辑上的“时间输入控件”。

这种控件, 我叫他“组合元素”。

对于这些组合元素, 在 framework 里不做特殊处理是可以的, 前提是每一个基本元素都能方

便的识别出来。但实际往往并不如此乐观, 比如页面开发人员为了使得组合元素更像一个整

体, 他们结合得都比较紧密, 以至于没有一个“可见的标识”来标识其中的某一个基本元素,

比如上面例子中的 Minute 输入 list。

用 Class 来封装

这种情况, 就需要针对组合元素来做专门的封装。 在封装组合元素时使用到了 VBS 里的

Class。 VBS 对面向对象支持是相当之弱, 这也是 framework 里仅有的使用 Class 的地方。

对于封装组合元素的 Class, 有下面的接口定义。以上图为例。

Class DateTimeInput

Function Init(objContainer, strMark)

Function Exist(nSec)

Function SetValue(strVal)

Function Value()

End Class

' CombinedInput 的工厂函数, 负责创建 CombinateInput 实例

Function GUI_CombinedInput_Create(xtype, objContainer, strMark)

对于 DateTimeInput 组合元素, starMark 是第一个 Edit 的 Label, 也就是

“Begin-Date”或者“End-Date”。 基本算法是, 找到第一个 WebEdit, 然后向上

找到 TR, 然后在到 TR 的第二个 TD 里的两个 WebList 就分别是 Hour 和 Minute。

CombinedInput 与 GUIElementQuery 的集成

一个很自然的想法是把 CombinedInput 与 GUIElementQuery 结合起来。 在实践中, 我也是

这样做的。 但是更进一步, 把 CombinedInput 集成进 SmartInput 是不可以的。因为这会把

SmartInput 弄糊涂, 它没法判断一个基本 Element 到底是独立的一个还是属于某一个

CombinedInput。 那也就是说 FormInput 里对 CombinedInput 是必须指定类型的。

Page 22: Different QTP.v3

Different QTP

Page 22

GUI 元素库:快捷方式

所谓的快捷方式, 其实是 GUI 元素库的用户接口。 到目前为止, GUI 元素库主要功能就有

了。 但还差一步, 就是提供一个简单高效的接口给调用者。这个专门的用户接口集合将会更

进一步增强调用者“所见即所得”的感觉。

缺省主窗口

大多数情况下, 一个应用程序都只有一个主窗口。 其他部件要么是在主窗口里面, 要么是主

窗口的子窗口, 比如对话框。既然如此, 那么 container 对象就不需要在每次函数调用时指

定了,而只需在测试程序初始化的时候 设置, 或者在主窗口切换时指定。

GUI_SetMainWindow(objContainer)

针对每一个元素类型提供一个接口

对于所有的基本元素和组合元素, 都提供一个查询的快捷方式。另外对于 Find 方法和 Get 方

法要分别提供一套 API,下面函数名的 F 前缀表示 Find, G 前缀表示 Get。对于 Web

Application, 有下面的接口函数。

FLink(strMark)

FEdit(strMark)

FList(strMark)

FCheckBoxButton(strMark)

FRadioButton(strMark)

FButton(strMark)

FImag(strMark)

FTable(strMark)

FDateTime(strMark) 'Combined input

... ...

GLink(strMark)

GEdit(strMark)

GList(strMark)

GCheckBoxButton(strMark)

GRadioButton(strMark)

GButton(strMark)

GImag(strMark)

GTable(strMark)

Page 23: Different QTP.v3

Different QTP

Page 23

GDateTime(strMark) 'Combined input

... ...

FormInput(strInputMap, strTypeMap)

在这样一套 API 的支持下, QTP 代码编写可以成为一件很轻松而快乐的事情:),请看下面

登陆的例子。

登陆

GEdit("Login name").Set "ABC"

GEdit("Password").Set "111111"

GCheckBox("%daylight%").Set "ON"

GList("Time Zone").Select "GMT+12, Marshall Islands Time, Fiji Time"

GButton("Login").Click

登陆的 FormInput 版

FormInput("Login name=ABC; Password=111111; %daylight%=ON; Time

Zone= GMT+12\, Marshall Islands Time\, Fiji Time", "")

Page 24: Different QTP.v3

Different QTP

Page 24

GUI 元素库:用 Screen-Field 解决复杂组合元素

有一些 Client-Server 模式的企业应用程序, 其 GUI 输入元素是非常复杂的, 不但一个 Form

里包含的输入元素多, 而且有很多组合类型。 比如下面的例子。(TODO:resource select

window)

对于这种情况, 使用前面描述的 CombinedElement 方案并不合适。 从设计的角度, 这里有

一个“代码复杂度”和“用户接口的简洁”的平衡问题。原因是 CombinedElement 和

ElementQuery, FormInput 是耦合的,是你中有我, 我中有你的状态。 如果是小型的, 少

量的且重复使用率高的 CombinedElement, 那么是合适的。 因为通过添加少量的代码就能

把 Combined Element 集成到一个统一的框架里, 提供给调用者一个统一的接口。 但是要集

成大量且复杂的 Combined Element, 那么结果就会是把 GUI 元素库代码搞得一团糟。

所以对于这种情况, 还是在 GUI Element 库的外面开发一个专门的框架比较好。这就是将要

介绍的 Screen-Field 方案。

首先看一下定义。

Field

Field 是一个逻辑上的 Input 单元。 它可以是基本 Input 元素, 也可以是自定义的组合元素。

一个 Field 可以被一个且仅有一个标识符定位( 同基本 Input 元素一样)。

一个 Field 可以被设置值。 对于基本 Input 元素,框架会调用 GUI_GetElement 来定位该元素。

对于组合元素, 每一个 Field 类型有一个对应的设置值的方法, 如下

'@param objContainer a window that the field resides

'@param strMark a string to identify a specific field

'@param strVal the value to be set

GUI_Field_FieldName_Set(objContainer, strMark, strVal)

Screen

Screen 简单来说就是 Field 的集合。

每一个 Screen 都有一个名字, 还有一个对应的 Screen 配置文件, 以"screen"作为后缀名,

比如 Login.screen。 Screen 配置文件里定义了 Field 的类型。 比如

#in Login.screen

Login name=Edit

Password=Edit

%daylight savings%=On

Time zone=List

Screen 支持 SmartInput, 所以基本 Elemen 无需自定类型。碰巧上面的 Login.screen 里所有

元素都是基本元素, 也就是说里面可以是空的。 但这个文件还是必须要的。

Page 25: Different QTP.v3

Different QTP

Page 25

Screen 的接口函数如下

GUI_Screen_Set(objContainer, screenName, strValueMap)

Screen-Field library 和 GUI element library 的关系

可见 Screen-Field library 依赖于, 但不侵入 GUI element library。 所以无论有多少复杂的

Field, 都不会丝毫影响到 GUI element library。

+-----------+ +------------+

| Screen | 1 1| xxx.screen |

| +---------->|------------|

+-----+-----+ | field=type |

1| | |

| | |

| | |

n| +------------+

+-----v-----+

| Field |

+-----------+

|-----------------------|

|Screen-Field library |

|-----------------------|

|

|

|

|---------V---------|

|GUI Element Library|

|-------------------|

Page 26: Different QTP.v3

Different QTP

Page 26

总结:关于 framwork 设计的一些思考

到这里, 我已经把这个 framework 里最重要的部分, 开发 testcase 的基石, Function

Library 介绍完了。

在设计这个 Function Library 的时候, 有两个最重要的抽象层, 一个是对产品业务逻辑的封

装, 一个是对 GUI 元素访问的封装。 对于业务逻辑封装, 最重要的原则是”把业务逻辑和

测试逻辑严格分离“, 另外就是不要使用 QTP 提供的 Reusable Action。 对于 GUI 元素的封

装, 我舍弃了 QTP 本身提供的对象库, 而完全用描述性编程的方式建立了一套”所见即所

得“的 GUI 对象查询库。

为什么是这样子抽象?是两层抽象, 而不是一层(仅仅提供一些 GUI 元素操作的封装), 或

者三层(提供 data-driven/keywork-driven 类似的用户接口)? 我的经验告诉我, QTP 软件

测试项目最大的挑战其实是可维护性。无论你是用 record 还是层层封装, 基本上你都能实现

一个 testcase。 但是真正的问题在于当 testcase 的总数上升到成百上千的数量级,代码库是

否还能保持清晰? Testcase 是否能够适应今后产品的变化?维护人员是否能快速定位错误?

当前的抽象模型是我认为对我目前项目最合适的。

我始终认为,好的 framwork 设计者应该尽可能取悦 framwork 的用户。 一个 QTP test

framwork 的用户其实有三类,

业务逻辑封装函数的开发人员

testcase script 开发人员

testsuite 维护人员

一个 framwork 的设计者,最好同时也从事上面提到的三种类型的开发 ,这样才能”叫好又叫

座“

最后, 是没有最好的 framework, 而只有最合适的 framework。 如前言说讲, 这是一种“非

常规”的开发模式,特别是 舍弃了很多 QTP 提供的智能工具。QTP 提供这些工具的目的是

为了支持它的”keyword driven”这样一种开发模式。客观的说, 这是一种易用,快速的开发

模式,但个人之见是仅仅适合小型,且短期的项目, 而且和普通的编程语言开发模式非常不

一样(java/c#/c++)。基本上, 我是把 QTP 测试的开发模式, 转变成了普通语言开发的模

式。

我曾经一位在 HP 工作过的同事告诉我, 在 HP 内部也是用 QTP 推荐的模式, 而且其项目也

是不小。 所以我有时候怀疑这是不是因为我使用 Java 太长以至于形成了思维定势, 也许

QTP 本身的模式并不是那么脆弱, 低效。 但是无论怎么样, 我现在使用 的模式也有成功案

例的支持, 所以我想可能这个也是一个因人而异的事情吧。 只是希望一些新的想法, 方法的

出现能够激发更多人的灵感, 把 QTP 测试项目开发模式更加完善, 进一步提高 QTP 测试开

发人员的生产效率。

Page 27: Different QTP.v3

Different QTP

Page 27

总结 2:适应变化和可测试性

适应变化

相对于单元测试, 接口测试等自动化测试, 对基于 GUI 的测试面对一个很大的问题是 GUI

是容易改变的。

对于这种情况, QTP 给出了 SmartIdentification, 试图让工具更智能, 实践表明大多数时候

它只是在帮倒忙。

有些公司出台了一些 Policy, 限制 Developer 对 GUI 元素识别标识符(比如 ID)的修改。比

如我现在的公司就提出过修改 GUI 的一些 policy, 结果是不了了之。因为很多 Developer 根

本就记不住那些约束, 或者不愿意被约束。另外, 有些时候, 一些 GUI 元素是由工具生成

的, 其 ID 变化不在 Developer 的控制之中。

所以, 在设计 GUI 元素库的时候, 如何应对 GUI 的变化是需要重点考虑的。 我的答案是,

不去试图控制变化, 而是提高自己适应变化的能力。这就是“所见即所得”识别方式背后的

动因。相对于“对象库”方式, “所见即所得”不仅仅带来了快速定位 GUI 元素的能力, 它

还让修改变得很简单。当 GUI 元素修改了, 如果你只需要花几秒钟就能修改好的你的代码,

那么你也不会太在意这种修改。

可测试性

有一本叫“重构”(refactory)的书说, “重复的代码”是代码臭味的一种。当代码具有臭味时,

你就需要去重构它。代码的重用率一定程度上也能反映出代码的整体质量。 简单来说, 通过

copy-paste 构建的程序肯定比通过抽象构建的程序代码质量差。

在我看来, 代码重用率高的程序一定程度上也是可测试性高的程序。 GUI 元素库对于元素定

位的每一种基本方法(包括组合元素定位)是针对某一类型的 GUI 元素的抽象。一个程序,

如果它的 GUI 部分的代码重用率高,那么就有更多的 GUI 元素其定位标识是有规律的。 那么

同样数量的定位方法就能覆盖更多的 GUI 元素。 一个程序如果是通过 copy-paste 建立起来的,

因为 paste 之后, 程序员是可以任意修改代码的, 这种修改可能破坏了原有的 GUI 元素定位

规律。所以结果就是, 不得不写更多 GUI 元素定位基本方法(或者组合元素)来覆盖所有的

情况。

所以, 我发现, 使用这个 GUI 元素库, 越是高质量的软件,其测试套件开发起来越轻松。

Page 28: Different QTP.v3

Different QTP

Page 28

测试执行流程与 Mngt Tools

一套测试套件设计出来之后, 它的价值在一遍一遍的执行中体现出来。如何能最大化的优化

执行的流程, 是 Mngt tools 的任务。 在我的项目中, Mngt Tools 是由一些 VBS 脚本和 BAT

脚本构成。

下图是 Mngt Tools 所包含的脚本。用户直接调用的脚本都是 BAT 脚本, 实际实现在 VBS 脚

本里。VBS 脚本放在 Script 文件夹里面, 如下图所示。

测试项目的执行流程定义如下。

+-----------------+

| Prepare Test |

+-------+---------+

|

|

+-------v---------+

| Execute Test |

+-------+---------+

|

|

+-------v----------+

| Generate Report |

+-------+----------+

|

|

+-------v----------+

| Analyze Result |

+------------------+

Prepare Test

在准备 Test 阶段, 最重要的是产生一份所有要执行的 testcase 的清单,在这里是

TestCaseList.txt。显然, 可以通过扫描 Test Case Repository 目录来达成。 这

是通过脚本 GenTestCaseList.bat 来完成的。

Page 29: Different QTP.v3

Different QTP

Page 29

另外, 被测试产品的版本号是一个非常重要的信息, 必须在 product.version 文件里指

定。

Execute Test

通过调用 Batchrun.bat,所有定义在 TestCaseList.txt 里的 testcase 都会被顺序执

行。执行过程中的所有 log 会记录到 Run.log 文件里。

执行结果所有信息会在一个目录里保存, 如下图所示。

其中,Batchrun.bat 会把每一个 Testcase 的执行结果和错误信息(如果有的话)保存在

TestResult.txt。这个文件会在"Generate Report"阶段被转换成 TestResult.html。

Execute Test Twice

因为 GUI 测试天然的不稳定性, 有时候, 你会希望执行两次,并且取两次运行合并后的结

果作为最终结果。合并的算法是, 一个 testcase,只要有一次通过, 就认为是通过的。

这钟方式能够有效的消除 不稳定性导致的 testcase 失败的机会。

对于这种方式, 执行过程是下面这样.

1. Batch run all test cases in TestCaseList.txt)

2. Restore test environment

3. Batch run only failed test cass

4. Merge test results

BatchRun_Rerun.bat 能自动的完成上述的流程。测试结果目录是下面的样子。

Page 30: Different QTP.v3

Different QTP

Page 30

注意这里有三个 TestResult 文件。 TestResult.txt, TestResult_Rerun.txt 和

TestResult_Merged.txt,分别是“第一次”,“第二次”,“合并后”的结果。

Generate Report

在所有 testcase 执行完之后, 调用 GenReport.bat, 会为该次执行产生一个 HTML report,

如下所示。这个动作已经集成在 BatchRun.bat 和 BatchRun_Rerun.bat 脚本里面。 不过也可

以在任何时候调用 GenReport.bat。

这个 Report 由总到分, 逐级展示测试结果。最上面是整个 testrun 的结果, 下面是

TestModule 和 TestSuite 的结果, 最下面是每一个 testcase 的测试结果。

在查看每一条 Testcase 结果记录的地方, 你可以立即获得错误信息和当时的

Screenshot, 他们都是对于结果分析非常有用的信息。 实践表明, 这种便利性可以大大

Page 31: Different QTP.v3

Different QTP

Page 31

加快结果分析的效率。 如果你需要在 Result Viewer 里面查看更详细的信息, 那么可以

打开 ResultXML,这个文件就是 Result Viewer 需要的文件。

Analyze Result

为了方便结果分析, 需要用到 GenExcel.bat 这个脚本。 它可以根据测试结果文件

(TestResult.txt)文件, 产生一个 TestResult.xls 文件。 利用 Excel 的能力, 你

可以方便的根据测试结果过滤所有 testcase, 并且可以随时记录下分析结果。

如果需要汇报测试执行结果, 那么可以在邮件正文给出汇总的信息,并附上

TestResult.html 和 TestResult.xls 两个文件。 这样你的邮件基本上包含了各种

stakeholder(team leader, manager, developer...)可能感兴趣的信息, 而且还

不会显得凌乱。

Page 32: Different QTP.v3

Different QTP

Page 32

Project 目录架构

在安排 QTP test Project 的目录树的时候,需要考虑到下面的问题

如何组织 Function Library, 如何引用 function library 文件?

如何组织 Testcase, 如何在 testcas 间共享数据/代码?

如何配置静态参数, 运行时环境和全局测试数据?

如何组织 Function Library?

按照前面的设计方法搭建的 Function Library 会包含大量的函数, 最终代码量也会比较庞大

(基本上取决于业务逻辑封装函数的多少)。比如最大的一个项目, 其 Function Library 总共

代码量达到 11000 多行, 假设 1/4 是注释, 那么实际代码行数 大约是 8000 多行, 按照一个

函数 20 行代码, 总共大约 400 多个函数。

为了维护这么多函数, 肯定要分成多个文件, 按照 Module 组织起来。 但是 QTP 对于外部

文件引用支持很差。 基本上 10 个以上的外部文件引用就让人抓狂了。

所以我才用了一种折中的方法。 首先, 一个 Module 一个 vbs 文件, 这样代码维护最方便。

然后有一个脚本能把所有的 Module 文件合并为一个大的 qfl 文件, 方便 QTP 引用。

下面是一个真实项目的 Library 目录。

BIZ

业务逻辑封装函数文件所在目录。

FRM

VBS 运行时扩展库文件所在目录。因为 VBS 的运行时库实在是太简陋(当然, 这个也是情

有可原, 毕竟是 70 年代的语言), 所以必须要写一些基本的扩展例程。 比如断言机制,

Log 机制, 动态数组, 动态字符串, 文件处理, 时间格式化, 等等。 接口基本上参照 JVM

做可以了 :)。

把这些基本功能作为 VBS 扩展库, 和项目的 Function Library 分开有助于你维护一个跨项目

的 VBS function Library, 更有效的利用代码。

Page 33: Different QTP.v3

Different QTP

Page 33

所以, 我的项目里, 从 testcase script 开发来说, 是下面的层次

+----------------------------------------+

| |

| TestCase Script |

| |

+---------------------------------------->

| |

| Function Library |

| |

+---------------------------------------->

| |

| VBS Extended Library |

| |

+----------------------------------------+

GUI

GUI 封装库所在目录。除了前面介绍的 GUI 元素库用来定位一个 GUI 元素, 还有对常用的

GUI 元素操作封装的函数。 比如下面是一个对 Java Tree 的封装。

Screen

Page 34: Different QTP.v3

Different QTP

Page 34

是所有 Screen 定义文件所在的目录。

UTI

其他功能函数所在的目录。

AllInOne_Lib.bat AllInOne_Lib.qfl

AllInOne_Lib.bat 把所有上面目录里的 vbs 文件合并到 AllInOne_Lib.qf 里, 方便 QTP 引用。

INIT.qfl

初始化函数库里 Module 的函数所在文件。因为某些 Module 的初始化可能有依赖关系, 所以

提供一个专门用来初始化函数库的地方。

Library.cfg

配置函数库里 Module 的配置文件. 这个文件内容必须满足 VBS 的语法.因为他也会合并到

AllInOne_Lib.qfl 里。在实践中我发现, 绝大多数 Module 其实都不需要外部配置, 因为毕竟

这只是一个测试项目的 Library,对灵活性的要求远没有开发产品的 library 那么高。

如何组织 TestCase?

TestCase 被组织在一个四层树状结构列. 如下所示.

+-------------+

| TestRepo +-----> Test.qfl

+-+-----------+

|

| +---------------+

+---->| TestModule +-----> TestModule.qfl

+--+------------+

|

| +----------------+

+->| TestSuite +------> TestSuite.qfl

+--+-------------+

|

| +-----------------+

+->| TestCase |

+-----------------+

最顶层是 TestRepo(TestCase Repository Root)。下面是 TestModule, 一般对应于产品的一个

功能模块。再下面是 TestSuite,一般对应于一个特定的测试点。最下面是 TestCase。

对于 Testcase 组织来说, 有两个重要的问题,

1) 如何在 TestCase 之间共享数据和函数

对于第一个问题,每一个 TestRepo, TestModule 和 TestSuite 都有一个公共库文件。 这个

文件会被它所包含的多少有 Testcase 引用。 所以在这个文件里, 一般配置特定范围的, 公

Page 35: Different QTP.v3

Different QTP

Page 35

共数据和公用方法。 举个例子: 如果某一个 Testsuite 里的 部分/全部 TestCase 都依赖于一

个特定的对象。 那么可以把这个对象的数据, 比如名字, 配置在 TestSuite.qfl 里。因为此

TestSuite 里所有 TestCase 都引用此文件, 所以数据得以共享。

对于任意一个 Testcase, 它的 Resource 引用像下面的样子。

Library\AllInOne_Lib.qfl

..\..\..\Test.qfl

..\..\TestModule.qfl

..\TestSuite.qfl

Library\INIT.qfl

2) 如何实现 Setup 和 Cleanup

对于第二个问题, 通过增加两个特殊的 TestCase, 00_Setup 和 99_Teardown 来解决。一

个 TestSuite 里如果有这两个 TestCase, 那么 00_Seup 会被最先执行, 99_Teardown 会被

最后执行。 下面是一个实例。

如何配置运行时环境, 和集中式测试数据?

对于一个 QTP Test Project, 环境数据必须是可配置的,而不是绑定到代码里。

我使用了 QTP 风格的 Environment 配置文件, 方便在运行时被 Load 进来。 QTP 的

Environment 配置文件是 XML 格式,位置在 Conf/Environment.xml, 示例 如下

<Environment>

<Variable>

<Name>ServerIP</Name>

<Value>172.17.68.40</Value>

</Variable>

</Environment>

在运行时, 通过 Framwork 加载吃文件, 然后就可以用

Environment.Value("ServerIP")的形式在代码里直接获取相应变量的值。

Page 36: Different QTP.v3

Different QTP

Page 36

关于集中测试数据。 测试数据和测试逻辑分开是一句在自动化测试领域流程甚广的一句话。

但对于 QTP Project, 定义一个外部的,集中式的的 Testdata 文件却是不正确的。因为

各个 TestCase 对数据种类的要求是不一样的, 没办法统一起来,强行统一就会导致大量的

column。 我见过在 Excel 里面用到“T”这个 column,也就是用 20 个 Column,简直是

维护的灾难。

补充一下, “测试数据和测试逻辑分开”这句话在下面两个场景是对的。

1) 第一是针对一个具体的 testcase, 在编写 testcase 的时候, 把 testdata 定义在

文件的顶端, 不要和 teststep 混在一起。

2) 第二是针对某一批具有相似测试逻辑的 Testcase 集合。 把 Testdata 定义在单独的数

据文件里。

典型的 Project 目录结构和 Project.cfg 文件

下面是一个典型的 Project 的目录。

需要特别说明 Project.cfg 文件。 Project.cfg 是一个用来配置项目的文件。无论在执

行 testcae 的时候,还是在执行 Mngt Tool script 的时候, 这个文件都会被 load 进

来。因为这个特性, 几乎任何配置信息都可以放在这个文件里。

Project.cfg 本身是一个 VBS 文件,不过它只包含参数配置, 而不应该包含任何函数代

码。

在我写这个 Framwork 的时候, 已经有一些老的 QTP 项目在运行。这些老的 QTP 项目的目

录结构和上面定义的标准结构是类似的,只是名字不一样。 为了能够把老的 QTP 项目迁移到

新的 Framwork,在编码的时候, 所有目录名和重要的文件名都是通过变量引用,而不是

hardcoded。这样只需要把目录名字作为配置信息写在 Project.cfg 里,就能够解决迁移

时目录名不一致的问题。下面是一个例子。

'''''''''''''''''''''''''''''''''''''''''''''''

'define project directory

'''''''''''''''''''''''''''''''''''''''''''''''

Page 37: Different QTP.v3

Different QTP

Page 37

Cfg_Dir_Conf = "Conf"

Cfg_Dir_Doc = "Doc"

Cfg_Dir_Library = "Library"

Cfg_Dir_ObjectRepo = "ObjectRepo"

Cfg_Dir_Run = "Run"

Cfg_Dir_TestActions = "TestActions"

Cfg_Dir_TestData = "TestData"

Cfg_Dir_TestRepo = "TestRepo"

Cfg_Dir_TestResult = "TestResult"

Cfg_Dir_TestTools = "TestTools"

Cfg_File_Environment = "Conf\Environment.xml"

Cfg_File_TestData = "TestData\TestData.xls"

Cfg_File_ObjectRepository = "ObjectRepo\ObjectRepository.tsr"

Page 38: Different QTP.v3

Different QTP

Page 38

QTP 使用中的陷阱

不要使用 Reuable Action

用 Function, 不要用 Reusable Action。 没有一种通用的语言里有 Reusable Action 这个概念。

而且通过 Function 等一些标准的程序设计语言的元素, 你能够实现任何 Reusable Action 可

以实现的功能, 而且更好, 更快, 更易于维护。

以前我还不能肯定这一点。 现在我能肯定的告诉你, 因为我已经做了好几个 QTP 项目, 实

现了上千的 Testcases。

不要用 Smart Identification

有一天, 我发现一个奇怪的现象, 一个 testcase 里某一个点击 logout button 的步骤运行非常

慢, 大概要 20 秒,但是最终它还能成功点击。 不巧的是每一个 testcase 几乎都会点击这个

button, 所有我还必须把这个问题找出来。 最后发现这是因为 button 的 name 有了变化, 但

是因为我 enable 了 Smart Identification, 所以 QTP 会试图去适应这个变化, 但是这个“适

应”的效果非常不理想。

我认为测试开发者应该完全控制对象的识别。把选择权交给对被测程序业务一无所知的工具是

毫无道理的。我想不到任何使用 SmartIdentification 的原因。 所以,从那之后, 任何

Testcase 的 Smart Identification 我都禁止了。

不要在相对路径识别之 base 目录里添加两个或以上目录

曾经, 我接手了一个 QTP 项目。 开始的时候我根本就不能把哪怕一个 testcase 成功跑起来。

于是我去问起开发者, 他告诉我需要把某一个 Reusable Action 添加到"folders"里面。

QTP 有一个智能相对路径识别的能力。 其配置在 Menu: Tools->Options->Folders。 我的建

议是这里只放项目根目录。其他目录都不要放进去。

同 Smart Identification 一样, 我不理解为什么 QTP 需要这种灵活性。

不要用 keyword view, 而是提供业务逻辑封装层

如果你要让你的 testcase 简单, 直接,那么你应该通过合理的抽象提供完善的业务逻辑封装

层, 它会使得你的 testcase script 读起来像 testcase descript 一样。 这个时候, 你根本不需

要 keyword view。

不用 Reporter.ReportEvent 而用 Assert

当执行一个 Testcase 时候, 如果遇到错误, QTP 支持两种操作。"stop run" 和"proceed to

next step"。(注意, 其实还有另外两个选项。 "pop up message box"是针对开发时使用。

"proceed to next action iteration"不常用。 所以不讨论。)

Page 39: Different QTP.v3

Different QTP

Page 39

基本上, QTP 鼓励"proceed to next step"这种方式:如果出现了错误, 那么记录错误, 但

testcase 仍旧会执行下去。

对于 QTP 检查到的错误是这样, 对于测试脚本自己检查到的错误也是如此。 QTP 提供了一

个错误报告机制。

Reporter.ReportEvent micFail, stepName, errorMsg

这种处理方式违背了一个程序设计的基本原理:如果出现不能处理的错误, 那么就抛出错误。

实际上如果出现了错误, 执行下去是没有意义的。 我猜想 QTP 之所以会支持这种方式是因

为, 如果出现错误马上退出, 那么 testcase 里的负责 cleanup 的代码就无法执行。 想想看

JUnit 里的 Setup 和 Teardown 机制, 在 VBS 里是无法实现。

即使考虑到这点, 这样做还有一个问题,导致我不采用它的原因。 那就是不能在 batch run

的结果报告里显示错误。 如果采用"stop run"的方式, 那么 batch run 脚本能够获得导致

testcase 退出的错误信息。 然后在产生 report 的时候, 能够呈现出来。 再结合错误时刻的

screenshot, 实践表明, 大多数错误都能在阅读 report 的时候确定原因。 下面是一个

Report 里一个失败 Testcase 的呈现情况。

Page 40: Different QTP.v3

Different QTP

Page 40

如果使用 Reporter.ReportEvent 的方式, 那么在 Report 里你看不到任何错误消息,只能在

Result View 里面打开 result xml。这是一个耗时的操作,因为 Result View 很慢。 大多数时

候,你是在等待 Result View 把测试结果渲染出来。

为了更好的支持 checkpoint, 我在 framwork 里定义了一套 Assert 函数, 可以对大多数情况

下的断言语义提供直接的支持, 比如“相等”, “不等”, “包含”, “匹配”, 等。在

Assert 函数里面, 会抛出一个 Error。 下面是一个从 popup 窗口读取信息, 然后验证信息是

否正确的例子。

AssertMatch GUI_MsgBox_Msg("Information"),_

".*is invalid or you don't have the permission.", _

"Error message not displayed or correct"

如果该 Checkpoint 失败, 那么在 HTML report 里看到的信息像下面的样子。

AssertMatch: Error message not displayed or correct. Expected<,".*is

invalid or you don't have the permission.>, Actual<>

总结一下。在错误处理上,采用"stop run"的方式, 这也是所有程序设计里采用的方式。 在写

checkpoint 的时候,用 Assert 而不要用 Reporter.ReportEvent, 因为前者会帮助你更快的分

析错误。

Page 41: Different QTP.v3

Different QTP

Page 41

后记

QTP 软件测试开发应该是什么样的?怎样才是高效的开发方式?QTP 软件测试只需要三个月

的培训就能做好吗?

在开始进入 QTP 自动化测试开发之前和之中, 我有这样一些疑问。 慢慢的, 我体会到自

动化测试开发和其他通用的软件开发并无根本之不同。 也就是说软件开发中你能看到的一些

概念,比如: 模块化, 低耦合, 面向对象, 敏捷, 设计模式, 可维护性, 接口设

计。。。在 QTP 自动化测试开发中同样适用。 没有软件开发的基本素养, 同样也不能开发

好一个 QTP 自动化测试项目(我承认,或许能够开发出来, 但可维护性会很糟糕)。

但是, 因为从事 QTP 自动化测试开发的人员很多都是 QA 而不是 Developer, 所以 QTP

提供了很多方便的智能的工具来降低上手的难度, 并且打出了”不会程序设计也可以做 QTP

自动化测试”的口号(这种现象其实在别的开发领域也有, 比如“三十天掌握 J2EE”等等)

但是, 这些工具虽说是捷径但却牺牲了软件开发中一些基本的要素。简单来说, 这些工具

像积木, 可以搭房子,但不能建立大厦。所以, 我慢慢的抛弃了这些工具, 而转向了我所

熟悉的, 通常的软件开发方式来开发 QTP 自动化测试项目。

实践也证明了, 通常的软件开发方式也完全可以开发 QTP 自动化测试项目, 而且开发效率

和可维护性还更高(至少对我而言)。

以后, 我可能会慢慢离开 QTP 自动化测试开发。 所以, 特此把这个过程中的想法经验总

结出来,和所有人分享。 希望能对你有所启发和帮助。(DFL, 2012/09/07)