文章: 使用Sahi测试Dojo应用

谈及开源Web 自动化测试工具,相信很多人立刻会想到Selenium。本文给大家介绍的是另一款 开源Web 自动化测试工具Sahi。Sahi的网站上有关于与Selenium的对比,不过这不是我们今天探讨的主题。这篇文章的主要目的是向读者简单的介绍一下Sahi并分享一下个人使用Sahi测试Dojo应用的经验,希望对大家能有所帮助。

相关厂商内容

将Node.js用于数据密集型实时应用(DIRT)

缓存、NoSQL和网格计算——银行能教给我们什么?

Visual Studio 11 Beta 和 .NET Framework 4.5 Beta版免费下载中!

一.Sahi简介

1. Web2.0应用测试的困境

在开始介绍Sahi之前,我们一起来看看在开发Web 自动化测试(特指Web 2.0应用)时常面临的两大技术问题。

1. 页面元素的识别

根据个人经验,以下几点会给页面元素的识别带来障碍:

  1. 页面DOM树随着产品版本升级频繁发生变化。
  2. 页面元素没有id属性或者id属性值是动态的。
  3. 页面中具有相同属性的元素不止一个。

通常的解决方案:

  1. 针对第一点,恐怕没有太好的解决方案,所以只能随着产品的改变更新自动化测试的代码。关于这一点,如果能够存在某种元素识别方法能够以最小的代码改动应对产品变化,那就是最理想的了。
  2. 针对第二点,解决方案是要求开发团队对所有测试中用到的元素增加用以识别元素的静态属性值。这听起来容易,但做起来未必简单。一来,开发团队通常以开发新产品功能为最高优先级,所以不太愿意花时间在这上面;二来,如果产品本身使用了某种封装后的技术框架,恐怕也会存在技术上的局限。
  3. 第三点事实上是识别的精确性的问题,这个问题可以使用XPath和CSS选择器来解决。但两者对于相对关系的限制都过于严格从而导致代码不能灵活适应DOM树的变化,最终会使维护成本直线上升。但是它很“脆弱”,当DOM树结构的变化很容易导致XPath的失效。并且,CSS选择器的使用还必须考虑浏览器的兼容性问题,如果需要支持的浏览器种类比较多,代码编写的成本也会比较高。

那我们来看看Sahi关于元素识别的策略:

  1. Sahi倡导使用“可见”属性识别元素,也就是元素的value, title等属性。这样做的好处很明显,就是可以减少对Firebug, Chrome Developer Tools的使用,从而提高开发效率。也就是“所见即所得”。当然,我们知道,只靠这些“可见”属性值是不够的。Sahi使用的元素识别方式是传入一个属性值,Sahi按照预先的设置进行查找。例如,_div(“name”)用来获取一个div, “name”或许是id也或许是name。Sahi允许用户针对每种元素类型定义新的属性并设置新的查找顺序,这也包括自定义属性名。
  2. Sahi提供了基于上下文的元素识别API。目前它支持三种方式:
    • _in,在某个DOM节点下查找某个元素 (这显然好过用XPath或者CSS选择器)
    • _near,在某个元素附近查找符合条件的最近的一个元素。这也是个很有用的定位方式。
    • _under,在某个元素下方查找符合条件的最近的一个元素(前提是,两个元素需要有相同的偏移量(offset)), 比如table中同一个column中的cell就可以用这种方式相对定位。
  3. Sahi API中所有的identifier参数都支持正则表达式,例如,_div(/name.*/) 用来识别所有以某种预属性值是name开头的div。

因此,Sahi基本上能够较好地解决前面提到的三大关于元素识别的障碍。

2. 页面等待

通常Web 2.0应用中有很多AJAX的应用。由于请求响应的返回是异步的,自动化测试程序如何决定是否可以继续下一个操作或者是开始验证呢?如果下一步操作在AJAX请求响应还没有返回时就执行了,毫无疑问会导致测试用例的失败,并且是误判。

通常的做法是:

  1. 等待固定的时间,比如5秒。多长的等待算是合理呢?如果时间设置过短,被测应用在远程,由于网络因素使响应变慢,测试用例很可能失败;如果时间设置过长,即便在正常响应时间情况下,仍然要等待同样的时间,无疑是浪费。
  2. 轮询界面上某个指定元素,直至它出现从而继续下一步操作或者是超时,测试用例判定为失败。这种做法的坏处在于:一、必须找到这个“指定元素”,这往往不是那么容易的;二、如果AJAX在你所测应用中很普遍,这种代码可能会充斥你这个测试程序,从而导致开发速度下降。

Sahi能够判断AJAX请求是否已经处理完毕,然后继续下一步操作,这一点对用户是“隐式”的,也就是说用户不需要写任何代码。事实是,绝大多数情况下用户确实不需要自己写代码处理页面等待的问题,但是,有时应用的某个功能是执行多个AJAX请求完成的(例如,长时间操作的进度条显示),此时Sahi便无法胜任。这种情况下,用户只能利用Sahi提供的等待固定时间以及基于条件等待的API自己编写代码实现页面等待。

2. Sahi的工作原理

文章: 使用Sahi测试Dojo应用_第1张图片

图1.Sahi架构图

 

Web自动化测试的本质就是模拟用户事件(单击、双击、输入文本等操作)获取结果状态并验证是否符合预期。如上图所示,Sahi的核心一个用Java编写的代理服务器。它位于Web应用与浏览器当中。当HTTP请求响应通过Sahi代理服务器时,便被注入了用来回放测试用例的Javascript。这些Javascript中,一部分是Sahi本身用来驱动脚本运行的代码,另一部分是用户代码被Sahi代理服务器解析成的Javascript。目前Sahi支持三种编程语言:Sahi脚本, Java和Ruby。

3. Sahi控制器

运行<SAHI_HOME>/bin/dashboard.sh可以启动Sahi的Dashboard窗口。Dashboard窗口中显示了所有Sahi预配置并且用户系统上存在的浏览器。如果需要手工添加新的浏览器,可以点击下方的Configure修改浏览器配置文件。

文章: 使用Sahi测试Dojo应用_第2张图片

图2.Sahi Dashboard

点击浏览器图标,会弹出相应的浏览器窗口(此时Sahi已经自动给浏览器配置了Sahi代理服务器)。

文章: 使用Sahi测试Dojo应用_第3张图片

图3.Sahi初始页面

在浏览器窗口中按住ALT键并双击鼠标左键,就会弹出Sahi控制器窗口。(通常这只在IE中工作,在Firefox和Chrome中你需要按住ALT+CTRL)。Sahi控制器可以工作在所有Sahi支持的浏览器上。录制和回放是Sahi控制器窗口中最重要的两个标签页。

 

录制标签页

文章: 使用Sahi测试Dojo应用_第4张图片

图4.Sahi控制器 – 录制标签页

输入文件路径后点“录制”便开始录制,点“停”即停止录制,非常简单。标签页的中部是一个对象识别器,在页面上按住CTRL键,并将鼠标左键悬停在某个元素上,对象识别器就显示出能够识别该元素的Sahi语句。另外,你可以在下方的输入框中直接输入Sahi语句并查看运行结果。

回放标签页

文章: 使用Sahi测试Dojo应用_第5张图片

图5.Sahi控制器 – 回放标签页

回放标签页不仅能够一次性运行脚本,还可以单步运行,甚至可以中途暂停,这给调试代码带了很大便利。点击下方的链接可以查看解析后的脚本以及运行日志等。

4. Sahi脚本

Sahi脚本基于Javascript,不同的是Sahi脚本中所有的变量必须带有$前缀。Sahi代理服务器负责将用户编写的Sahi脚本解析成Javascript并在Rhino引擎中执行(Rhino是一个开源的使Javascript运行于JVM的项目)。所以,Sahi脚本能够执行文件甚至数据库操作也就不足为怪了。Sahi脚本定义.sah文件中,但是所有直接访问DOM节点的函数必须定义在browser tag中。

二.使用Sahi脚本测试Dojo应用

下面与大家分享一些我个人使用Sahi测试Dojo应用的经验。为了使示例代码能够被读者方便地运行,选取http://demos.dojotoolkit.org/demos/form/demo.html 假设为我们将要测试的应用。这是一个用来演示Dojo表单Widget的页面。

1. 如何运行示例代码

  1. 下载及安装Sahi( http://sahi.co.in/w/using-sahi)
  2. 下载sahidojodemo.zip并解压缩到Sahi的userdata/scripts下面。解压后应该下面这个样子。
  3. <SAHI_HOME>/userdata/scripts/sahidojodemo/appobjs/JobAppFormPage.sah 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/tasks/JobAppFormTasks.sah 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/testcases/JobAppFormTests.sah 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/testcases/myapp.suite 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/testcases/testdata.csv 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/core.sah 
    <SAHI_HOME>/userdata/scripts/sahidojodemo/run.sh 
  4. 启动Sahi代理服务器。
  5. 运行Sahi的bin目录下的sahi.sh脚本,或者dashboard.sh也可以启动Sahi 代理服务器(该脚本用来启动Sahi Dashboard,同时启动Sahi代理服务器)。建议启动dashboard,这样你能清楚地看出哪些浏览器被Sahi探测到了。
  6. 如果Sahi Dashboard中显示了Chrome,你可以直接运行sahidojodemo下的run.sh。否则,你需要把run.sh中的chrome替换成你系统中存在的浏览器,比如firefox。
  7. 如果一切正常你会看到Dojo的Job Form Application应用被打开,然会进行了一系列操作后关掉。这时,如果一些正常,Sahi的控制台上会显示"Success"。如果失败了,你可以去sahi/userdata/logs/playback下面查看日志。

2. 设计原则

接下来介绍一下我用Sahi测试Dojo应用是遵循的几个原则。

1. 面向对象

面向对象早已不是什么新鲜事物。其实,UI自动化测试程序直觉上来讲可以采用过程式的编程模式,因为它本身就是将很多行为串接起来。为什么在测试Dojo应用时要使用面向对象的理念?原因很简单,因为Dojo Widget本身就采用了面向对象的思想。因此,在我的自动化测试框架中,每个Dojo Widget都对应于一个Javascript的“类”,不仅封装了DOM结构而且更便于代码重用。

2. 采用IBM框架(之前叫ITCL - IBM Test Community Leadership)

从事Web自动化测试的读者恐怕对IBM框架不会感到陌生。IBM框架由三层组成:应用对象、任务和测试用例。潜在于应用对象、任务和测试用例包之下的基本原理是:

  • 层次化的体系架构
  • 将“做什么”与“如何做”分离开来
  • 代码重用
  • 一致和清晰的组织结构
  • 快速增强的能力
  • 迅速的调试
  • 有效地组织文件
  • 启用协作
  • 学习他人

下面是对应用对象、任务和测试用例的解释说明:

应用对象:储存有关你的应用程序中的GUI元素信息。同时在这里也可以编写你的Getter 方法,这些 Getter 方法可以返回对象,使 调用者能够对这些GUI元素进行查询和操作。一般情况下,这些方法在Task层中进行调用。

任务:在这里你将编写可重用的方法,这些方法在你的应用程序中执行通用功能。同时在这里,你将编写可以处理和查询复杂的特定应用程序控件的方法。在任务中的方法可以被测试用例调用。

测试用例:导航一个应用程序,验证其状态,并记录其结果的方法。

3. 借助Label识别元素

通常页面上每个元素都会有一个label并且它是“可见”的。所谓“可见”,是指label的值是不需要借助于工具直接能看到的。例如id、name等,必须通过查看源码或者一定的工具,如Firebug查看其属性值。因此通过借助label的元素识别方法可以提高开发效率(因为你不在需要去用工具查看元素属性值了)。

3. 代码详解

三个目录appobjs,tasks以及testcases即是IBM框架中的三层架构,其中的JobAppFormPage.sah,JobAppFormTasks.sah以及JobAppFormTests.sah分别应用对象、任务和测试用例程序文件(Sahi脚本)。

Dojo Widget的封装

下面以DojoWidget和Textbox两个类为例讲解Widget的封装。

function DojoWidget($self) {
    this.getLabel = function () {
        var $widId = getAttribute($self, "widgetid")
        _set($labelText, getLabelTextByFor($widId))
        return $labelText
    }
    this.hasError = function () {
        var $class = getAttribute($self, "class")
        return $class.indexOf("dijitError") == -1 ? false : true
    }
}
var $DojoTextbox = function Textbox($elem) {
        var $self = findEnclosingWidget($elem, "dijitValidationTextBox")
        DojoWidget.call(this, $self)
        var $textbox = _textbox("dijitReset dijitInputInner", _in($self))
        this.setValue = function ($value) {
                _setValue($textbox, $value)
                var $current = this.getValue()
                _assertEqual($value, $current)
            }
        this.getValue = function () {
            return _getValue($textbox)
        }
        this.blur = function () {
            _blur($textbox)
        }
    } 

core.sah中定义了所有的Dojo widget类。所有的Dojo widget类都继承DojoWidget。DojoWidget定义了一些widget通用的函数,例如getLabel和hasError。$self变量通过函数findEnclosingWidget获得,这个变量代表了Dojo widget最外层的元素。此函数通过检查父节点中是否有widgetid属性,并且检查class属性的值是否包含指定的标示widget类型的字符串(例如,DojoTextbox的类型字符串是dijitValidationTextBox)来识别widget的最外层元素。Widget的继承通过call函数实现,它将$self传给DojoWidget类的构造器。$textbox的识别使用了_in函数,这种方法保证了元素识别的准确性。事实上,无论一个widget本身有多复杂,通过_in函数就可以将内部元素查找与外界隔离。大家或许注意到this.setValue函数中有个比较奇怪的地方,this.getValue()的返回值是先赋值给$current变量然后进行断言判断的。为什么不写成“_assertEqual($value,this.getValue())”呢?这是因为目前Sahi不支持这样的语句,或许将来会支持。

findByLabel的实现

function findByLabel($labelText, $className) {
    var $label = _label($labelText)
    var $wid = getAttribute($label, "for")
    _set($id, findIdByWID($wid))
    var $div = _byId($id)
    return new $className($div)
} 
<browser>
function findIdByWID($wid) {
    var $widget = dojo.query("[widgetid='" + $wid + "']")[0]
    return $widget.getAttribute("id")
} 
</browser>

通过元素label标识元素的原则通过findByLabel函数实现。它有两个参数,第一个是label的文本内容,第二个是目标widget的实现类。实现原理很简单。label元素的for属性值就是widget的widgetid的值。因此,我们通过widgetid就可以找到widget元素。但事实上,从下面大家看出来我们是先利用的dojo.query找到了元素对应的id,然后通过_byId获得widget元素。为什么用这种迂回的方法呢?根据Sahi的文档,理论上我们是可以通过修改concat.js文件增加widgetid查找属性(具体参见http://sahi.co.in/w/tweaking-sahi-apis)可以实现利用_div,_table或者_span等函数直接获得widget元素。不幸的是,当前版本中存在的一个bug导致自定义属性不能被识别。所以,目前只能先通过widgetid找id的方法迂回解决。另外,值得一提的是因为findIdByWID函数用到了dojo的库函数,因此它被定义在browser tag中。

数据驱动

Sahi自带对CSV,Excel以及数据库访问的函数。示例代码示范了如何使用CSV进行数据驱动测试。让我们一起来看看JobAppFormTests.sah中的testSimple函数。被注释掉的部分是一般的定义测试数据的方法。_readCSVFile函数加载testdata.csv到$data变量,它事实上一个两维数组。_dataDrive函数能够自动遍历数组数据调用fillForm函数。

function testSimple() {
    /* 
var $eduValue="masters" 
var $nameValue="my name" 
var $addressValue="Shanghai" 
var $stateValue="California" 
fillForm($nameValue,$eduValue,$addressValue,$stateValue) 
*/
    var $data = _readCSVFile("./testdata.csv")
    _dataDrive(fillForm, $data)
}

testdata.csv内容:

Tom, high school, Address1, Alaska
Mike, masters, Address2, Florida
John, PhD, Address3, Hawaii 

其他

另外,Sahi提供了类似于JUnit的测试框架。所有以test开头的函数都被认为是测试用例,如果有setUp和tearDown函数,它们会分别在每个测试用例运行前后执行。并且所有测试文件还是可以组织到一个.suite文件中作为一套测试用例运行。更详细的介绍,请大家参考Sahi的官方文档。Sahi也能支持拖放,大家可以参考示例代码中Slider widget的实现。文件上传是很多Web自动化测试的局限,不过,Sahi得益于它Proxy的架构也实现了文件上传功能。

三.结束语

总的来说,Sahi是一款不错的Web自动化测试工具,尤其是它对元素关联查找的支持以及页面隐式等待的机制对Web2.0应用的测试是很有帮助的。希望读者阅读完本文能有所收获。如果,想了解更多关于Sahi的信息,请访问Sahi的官方网站( http://sahi.co.in/w/) 并且可以通过访问 http://www.slideshare.net/narayanraman 观看Sahi的推广演示文档。如果对Sahi与Selenium的比较感兴趣,可以访问 http://blog.sahi.co.in/2010/04/sahi-vs-selenium.html 。

四.代码下载

关于作者

沈锐目前工作于 IBM 中国开发实验室(CDL)。主要从事 IBM 存储产品的 Web UI 测试,如 IBM Storwize V7000、SVC 以及 SONAS 等。

 

 

感谢 郑柯对本文的审校。

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

你可能感兴趣的:(测试,文章,sahi)