求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
利用Sahi脚本自动生成代码加速Web自动化测试开发
 
作者:沈锐 , 发布于2012-5-4
 

本文是《使用Sahi测试Dojo应用》的延续。在《使用Sahi测试Dojo应用》中,我们谈到了ITCL架构(应用对象层,任务层以及测试用例层)。本文向大家介绍如何编写一个Sahi的脚本以自动生成应用对象层的代码从而简化和加速Web自动化测试用例的开发。

一.概述

之所以有可能开发一个Sahi脚本来生成应用对象层的代码,主要得益于以下几个方面:

1)面向对象设计模式的应用

Dojo本身将页面中的控件用面向对象的模式封装成不同的widget,而本测试框架用不同的Javascript的函数映射到不同的Dojo的widget。这样的一种设计模式,使我们有可能通过搜索页面中的Dojo widget以自动生成用来实例化页面控件的代码。

2)强大的Dojo query以及Sahi的基于上下文的API

在后面的代码详解中,大家可以看到我们是如何借助Dojo query以及Sahi的基于上下文的API来搜索页面中的Dojo widget的。并且,由于Sahi本身支持browser端的Javascript的脚本,因此在我们的代码中可以方便地将Dojo API和Sahi API混用。

3)Sahi的基础-Rhino

因为有了Rhino的支持,Sahi可以进行本地文件的读写,因此使我们能够将生成的结果以文件的形式保存下来。甚至,如果需要的话,我们可以实现基于文件的代码生成模版管理。(关于代码生成模版管理,本文所附带的示例代码没有包含该功能,如果读者感兴趣可以自行尝试)。

简单来说,代码生成Sahi脚本的工作过程如下:

1.为每种Dojo widget定义一个Javascript函数。该函数的功能是为特定widget代码生成提供元数据。

2.使用者在调用代码生成函数时,可以传入一个数组以指定要生成的widget的名称,比如按钮、输入域等。默认,代码将生成所有支持的widget类型。

3.当代码生成函数被调用时,它使用Dojo query遍历DOM tree以搜索Dojo widget的最外层元素(通常是DIV并包含widgetid属性)。之后,将该元素交予具体的处理widget的函数生成声明代码。

4.如何知道通过何种方式可以实例化找到的Dojo widget呢?在Dojo widget函数的公共“父类”中,定义了若干“猜测”函数,例如guessByName、guessById以及guessByLabel。如果需要的话,具体的widget函数可以定义自己的“猜测”函数,例如DojoButton函数就定义了自己的guessByText函数(因为这个函数不具备通用性)。“猜测”的入口是一个叫guess的函数,具体的widget函数可以传递给guess一个数组以指定“猜测”的优先顺序,例如,[this.guessById,this.guessByLabel]就表明先检查widget有没有id属性,如果有就生成通过id实例化widget的代码,如果没有的话,就继续尝试“猜测”label的方式。如果所有的“猜测”函数都失败,就在Sahi的log中打印出一条信息,告诉调用者,无法生成这个widget的实例化代码。

5.最终,把所有生成好的代码语句拼接成一个字符串,保存到generated目录下的appobjscode.sah文件中。同时,这段代码也会打印在Sahi的log文件中。

二.如何运行代码

用来生成应用对象层代码的脚本位于压缩包的sahidojodemo/codegen目录中,有codegen.sah和main.sah两个Sahi脚本文件。codegen.sah定义了代码生成的核心逻辑而main.sah只是对其进行调用。我们使用的示例页面依旧是http://demos.dojotoolkit.org/demos/form/demo.html。读者只需在该页面上弹出Sahi控制器并运行main.sah脚本即可。下面是该脚本的运行效果图。

若要测试生成的代码是否工作,只需要把上图生成的代码粘贴到appobjs/ JobAppFormPage.sah中并运行run.sh。具体的操作步骤请参考《使用Sahi测试Dojo应用》的“如何运行示例代码”部分。

三.代码详解

下面对代码进行详细地解释。

1.函数概览

在codegen.sah中有如下一些函数(或者称做“类”)。

WidgetMetaData:定义全局widget“元数据”的结构。它包含widget名称、搜索模式以及处理“类”的名称三个属性。

AppObjsCodeGen:负责遍历页面搜索widget、调用相应的处理“类”生成代码并格式化代码。

DojoWidget:负责代码生成的核心逻辑。定义公共的“猜测”函数。

DojoTextbox:“继承”自DojoWidget。提供输入域widget的元数据。

DojoSlider:“继承”自DojoWidget。提供滑块widget的元数据。

DojoCombobox:“继承”自DojoWidget。提供下拉框widget的元数据。

DojoButton:“继承”自DojoWidget。提供按钮widget的元数据。

2.元数据的定义

元数据的定义分为两部分。第一部分是一个metaData的数组,它用来声明所有支持的widget类型,数组元素是WidgetMetaData。第二部分就是映射到每种Dojo widget的Javascript函数,如DojoTextbox等,它们提供了特定widget的元数据。

以下是metaData数组的声明。

 

var metaData=[]

metaData.push(new WidgetMetaData("textbox","dijitValidationTextBox","DojoTextbox"))

metaData.push(new WidgetMetaData("slider","dijitSlider","DojoSlider"))

metaData.push(new WidgetMetaData("combobox","dijitComboBox","DojoCombobox"))

metaData.push(new WidgetMetaData("button","dijitButton","DojoButton"))

本示例代码中之定义四种widget。

DojoTextbox的定义如下。

 

function DojoTextbox(domNode){

DojoWidget.call(this,domNode)

this._getIdentifier=function(){

var elem=_textbox("dijitReset dijitInputInner",_in(this.domNode));

return this.guess(elem);

}

this._getClassName=function(){

return "$DojoTextbox"

}

this._getSahiFuncName=function(){

return "_textbox"

}

}

_getIdentifier函数返回对guess函数的调用。之所以要传入一个elem参数,是因为name属性有时不在Dojo widget的最外层元素上,而是在其内部某个元素上。例如对输入域widget来说,name属性就是在其内部的input(type="textbox")元素上。 这里,我们用了Sahi的_in函数以保证只在该widget内部进行元素搜索。_getIdentifier函数返回三种类型的值:“label=...”,“=...”或者undefined。如果返回"label=...",说明该widget可以用label的方式实例化,于是就会生成形如var $name=findByLabel("Name *",$DojoTextbox)的代码。如果返回值是“=...”(这里的由“猜测”函数指定。例如,guessById会返回“id=...”而guessByName返回“name=...”),将生成形如var $name=new $DojoTextbox(_textbox("name"))的代码。其实,只要不等于“label”,都将以这样的方式生成代码。最后,如果任何一种“猜测”函数都失败了,就会返回undefined。那么,就会在log里看到红色error信息“Failed to guess the method for...”。对于_getClassName和_getSahiFuncName两个函数,大家不难看出,它们分别返回对应的widget的“类”函数名称以及相应的用来生成非label方式代码的Sahi函数名称。

代码行DojoWidget.call(this,domNode)用来实现Javascript中的“继承”。

3.如何搜索/识别页面中的Dojo widget

因为每种Dojo widget的class属性包含的值不同,因此我们可以通过这一点来搜索并识别页面中的Dojo widget。例如,如果其class属性包含“dijitValidationTextBox”,认为它是一个Dojo输入域widget;如果其class属性包含“dijitComboBox”,认为它是一个Dojo 下拉框widget。

输入域widget(通过“dijitValidationTextBox”识别)

下拉框widget(通过“dijitComboBox”识别)

下面我们一起来看看_formContent函数。

 

this._formContent=function(widgetNames){

var statements=[];

for(var i=0;i < widgetNames.length;i++){

var widgetName=widgetNames[i]

var widgetMetaData=this._findWidgetMetaData(widgetName)

var searchPattern=widgetMetaData.searchPattern

var handleClassName=widgetMetaData.handleClassName

var nodes=dojo.query("[class~='"+searchPattern+"']")

for(var j=0;j < nodes.length;j++){

    var node=nodes[j]

    if(!node.getAttribute("widgetid")){

        continue

    }

    var handle=eval("new "+handleClassName+"(node)")

    var statement=handle.getStatement()

    if(statement){

        statements.push(statement)

    }

}

}

return statements

}

首先,该函数根据widget名称在metaData数组中找到对应的元数据。之后,通过Dojo query搜索所有class属性中包含有searchPattern值的元素。当然,这当中有可能会有“假”的,所以,进而判断该元素是否有widgetid属性。如果有widgetid属性,表明是真的Dojo widget元素。接着,实例化对应的处理“类”并调用getStatement函数返回针对该widget生成的声明代码行。最后,把所有的代码行放入statements数组中并返回。

4.两种形式的声明代码行

在讲解_getIdentifier函数时,我们已经提到生成的代码有两种形式:通过label或者是通过元素属性值(如id,name等)。这个逻辑是定义在getStatement函数中的。

 

this.getStatement=function(){

var stmt=""

this.identifier=this._getIdentifier()

if(!this.identifier || this.identifier.substr(-1)=="="){

_log("Failed to guess the method for "+this._escape(this._outerHTML(domNode)),"error");

return;

}

var idKey=this._idKeyValue()[0]

var idValue=this._idKeyValue()[1]

var varName=this._formVarName(idValue)

if(idKey=="label"){

stmt=this.byLabelTemplate.replace('{className}',this._getClassName()).replace('{label}',idValue)

.replace('{varName}',varName)

}else{

var innerElemSahi=this._getSahiFuncName()+'("'+idValue+'")'

stmt=this.byAttrTemplate.replace('{className}',this._getClassName()).replace('{innerElem}',

innerElemSahi).replace('{varName}',varName)

return stmt

}

return stmt

}

函数的开始对_getIdentifier的返回值进行解析。之后,通过一系列的字符串replace操作生成最终代码行。

下面是两种不同的代码模版的定义。正如本文开头提到的,读者也可以将代码模版定义在文件中,使代码生成脚本从文件中加载模版定义。

 

this.byLabelTemplate='var {varName}=findByLabel("{label}",{className})'

this.byAttrTemplate='var {varName}=new {className}({innerElem})'

5.label的识别

guessById和guessByName比较容易理解。我们一起看看guessByLabel函数的实现。

 

this.guessByLabel=function(elem){

var idValue=elem.getAttribute("id")

if(idValue){

var label=dojo.query('label[for="'+idValue+'"]')[0]

}

if(!label){

var label=this.domNode.previousSibling.previousSibling

}

if(label){

return "label="+_getText(label);

}

}

因为绝大多数label都有一个for属性,该属性的值对应其附属的元素的id属性值。所以,guessByLabel首先得到widget元素的id属性值(注意,这个elem有可能是widget的一个内部元素,而不是widget的最外层元素),然后,通过Dojo query查找for属性为此id值的label。如果没有找到,它会通过相对位置获取label元素(通常label元素和其附属的元素是紧邻的)。最后,调用Sahi的_getText函数返回该label元素的文本信息。

为了配合这两种识别label的方式,core.sah中的findByLabel函数需要修改如下。所不同的是,findByLabel通过相逆的操作由已知的label文本识别对应的Dojo widget。

 

function findByLabel($labelText,$className){

var $label=_label($labelText)

var $wid=getAttribute($label,"for")

if($wid){

_set($id,findIdByWID($wid))

}

if($id){

var $dojoWidget=_byId($id)

}else{

var $dojoWidget=$label.nextSibling.nextSibling

}

return new $className($dojoWidget)

}

6.增加新的“猜测”函数

如果需要增加新的“猜测”函数,并且它具备一定的通用性,可以将此函数添加到DojoWidget函数中。具体写法可参照guessById和guessByName函数。另外,需要修改guess函数中的如下代码,这样才能把它加入到默认的“猜测”函数列表中。当然,你也需要考虑它的“优先级”,从而把它放在数组中合适的位置。

 

this.guess=function(elem,userGuessFuncs){

var guessFuncs=userGuessFuncs

if(!guessFuncs){

guessFuncs=[this.guessById,this.guessByName,this.guessByLabel]

}

for(var i=0; i < guessFuncs.length; i++) {

var guessFunc=guessFuncs[i];

rt=guessFunc.call(this,elem)

if(rt){

    return rt;

}

}

}

具体的widget处理函数也可以通过userGuessFuncs参数指定自己的”猜测“函数以及定义自己的”猜测“顺序。比如DojoButton,因为它的识别方式比较特殊,我们就在DojoButton中定义了guessByText函数,并把它作为第二个参数传给guess函数。读者如果有类似情况,可以仿照这段代码定义特定的“猜测”函数。

 

function DojoButton(domNode){

DojoWidget.call(this,domNode)

this._getIdentifier=function(){

var elem=this.domNode;

return this.guess(elem,[this.guessByText]);

}

this.guessByText=function(elem){

var widgetId=elem.getAttribute("widgetid")

var labelId=widgetId+"_label"

var label=_span(labelId,_in(elem))

var buttonText=_getText(label)

return "text="+buttonText

}

this._getClassName=function(){

return "$DojoButton"

}

this._getSahiFuncName=function(){

return "_span"

}

}

7. 生成文件

Sahi提供了_writeFile函数进行文件写操作。下面是main.sah的代码。$filePath定义了文件的保存路径。第三个参数是布尔型,表示是覆盖原文件还是进行追加操作 - True表示覆盖原文件。Sahi也可以读写CSV文件、重命名文件以及删除文件。具体请参见 http://sahi.co.in/w/miscellaneous-apis

 

var $filePath='generated/appobjscode.sah'

_set($fileContent,new AppObjsCodeGen().gen());

_writeFile($fileContent,$filePath,true);

四.结束语

本文向读者介绍了如何通过Sahi脚本生成应用对象层的代码来简化和加速Web自动化测试的开发。在实际应用中,由于不少读者会在Dojo widget的基础上再进行封装,因此,本文附带的代码未必可以直接使用。但是,读者可以借鉴这当中的思路。希望,本文对读者能有所帮助。


相关文章

微服务测试之单元测试
一篇图文带你了解白盒测试用例设计方法
全面的质量保障体系之回归测试策略
人工智能自动化测试探索
相关文档

自动化接口测试实践之路
jenkins持续集成测试
性能测试诊断分析与优化
性能测试实例
相关课程

持续集成测试最佳实践
自动化测试体系建设与最佳实践
测试架构的构建与应用实践
DevOps时代的测试技术与最佳实践


LoadRunner性能测试基础
软件测试结果分析和质量报告
面向对象软件测试技术研究
设计测试用例的四条原则
功能测试中故障模型的建立
性能测试综述
更多...   


性能测试方法与技术
测试过程与团队管理
LoadRunner进行性能测试
WEB应用的软件测试
手机软件测试
白盒测试方法与技术


某博彩行业 数据库自动化测试
IT服务商 Web安全测试
IT服务商 自动化测试框架
海航股份 单元测试、重构
测试需求分析与测试用例分析
互联网web测试方法与实践
基于Selenium的Web自动化测试
更多...