NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

利用 Sahi 脚本自动生成代码加速 Web 自动化测试开发

  • 2012-04-20
  • 本文字数:6251 字

    阅读完需:约 21 分钟

本文是《使用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 的基础上再进行封装,因此,本文附带的代码未必可以直接使用。但是,读者可以借鉴这当中的思路。希望,本文对读者能有所帮助。

代码下载


给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-04-20 00:003397

评论

发布
暂无评论
发现更多内容

干货|无源元件之——电感基础知识(详解)

元器件秋姐

科普 电感器 电感 电感元件 电子知识

华为云对象存储OBS超高性能数据存储能力,推进企业快速上云

爱尚科技

何惧内卷?华为云对象存储服务OBS工具随便拿出一个都很能打

与时俱进的时代

2022卡塔尔世界杯专题分析

易观分析

世界杯 体育

助力游戏厂商稳健发展,华为云大数据解决方案高效赋能!

与时俱进的时代

2022-12-29:nsq是go语言写的消息队列。请问k3s部署nsq,yaml如何写?

福大大架构师每日一题

云原生 k8s k3s nsq 福大大

华为云OBS对象存储服务:这个管家很贴心

爱尚科技

企业数据存储,还得看华为云对象存储服务OBS

爱尚科技

支持随时畅玩3A游戏,华为云大数据助力游戏厂商快速稳健发展!

与时俱进的时代

华为云OBS:让大数据的容器再无容量限制

爱尚科技

云渲染一张图大概多久?云渲染快吗?

Renderbus瑞云渲染农场

云渲染

亚信科技通信、交通行业数据库项目入选“星河”标杆、优秀案例

亚信AntDB数据库

AntDB 国产数据库 AntDB数据库

华为云CDN加速服务助你开启网络加速时代

爱科技的水月

APISIX Ingress 对 Gateway API 的支持和应用

API7.ai 技术团队

云原生 APISIX API Gateway Ingress Controller

华为云CDN加速,如何助力企业更好发展?

爱科技的水月

什么样的魔法棒,能让AI魔法师一夜成名?

脑极体

MatrixDB v4.6.0 发布,查询性能和图形化操作界面全面升级!

YMatrix 超融合数据库

Prometheus 存储引擎 超融合数据库 YMatrix MatrixGate

不止于快,华为云CDN加速服务对OBS桶文件加速的超实用技巧

爱科技的水月

【电商行业必备神器】轻松备战“双十一”,华为云OBS值得拥有

与时俱进的时代

什么样的魔法棒,能让AI魔法师一夜成名?

白洞计划

小米封杨:工业设备预测性维护及时序数据库选型

YMatrix 超融合数据库

工业4.0 超融合数据库 预测性维护 设备预测性维护 YMatrix

华为云CDN引领网站性能全面优化

爱科技的水月

存储空间不够大?试试华为云OBS对象存储服务

与时俱进的时代

华为云CDN加速服务,让企业用户上网“走高速”

爱科技的水月

助力网络碳中和 | 华为发布站点能源十大趋势

Geek_2d6073

存储数据不要愁,华为云来帮你!

与时俱进的时代

华为云微服务引擎0停机迁移Nacos?它是这样做的

科技之光

超融合一体流式引擎,打造分布式数据库新纪元

亚信AntDB数据库

AntDB 国产数据库 AntDB数据库

HTTPS基础知识

穿过生命散发芬芳

https 12月月更

华为云大数据BI解决方案助企业突破数据壁垒,加快企业数字化建设

与时俱进的时代

C#-使用Consul

kdyonly

C#

利用Sahi脚本自动生成代码加速Web自动化测试开发_DevOps & 平台工程_沈锐_InfoQ精选文章