使用 Sahi 测试 Dojo 应用

阅读数:5535 2012 年 3 月 20 日

话题:JavaScriptDevOps语言 & 开发架构

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

一.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 的工作原理

图 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 修改浏览器配置文件。

图 2.Sahi Dashboard

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

图 3.Sahi 初始页面

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

录制标签页

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

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

回放标签页

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