写点什么

应用 Selenium 和 Ruby 进行面向领域的 Web 测试

  • 2007-05-21
  • 本文字数:4478 字

    阅读完需:约 15 分钟

应用 Selenium 进行 Web 测试时,经常会遇到下面的几个麻烦问题:

  1. 大量使用 name、id、xpath 等页面元素。无论是功能修改、UI 重构还是交互性改进都会影响到这些元素,这使得 Selenium 测试变得非常脆弱。
  2. 过于细节的页面操作不容易体现出行为的意图,一段时间之后就很难真正把握测试原有的目的了,这使得 Selenium 测试变得难于维护。
  3. 对具体数据取值的存在依赖,当个别数据不再合法的时候,测试就会失败,但这样的失败并不能标识功能的缺失,这使得 Selenium 测试变得脆弱且难以维护。

而这几点直接衍生的结果就是不断地添加新的测试,而极少地去重构、利用原有测试。其实这倒也是正常,单元测试测试写多了,也有会有这样的问题。不过比较要命的是,Selenium 的执行速度比较慢(相对单元测试),随着测试逐渐的增多,运行时间会逐渐增加到不可忍受的程度。一组意图不明而且难以维护的 Selenium 测试,可以很轻松地在每次构建(Build)的时候杀掉 40 分钟甚至 2 个小时的时间,我就有曾有花 2 个小时坐在电脑前面等待 450 个 Selenium 测试运行通过的悲惨经历。因此合理有效地规划 Selenium 测试就显得格外的迫切和重要了。而目前比较行之有效的办法,往大了说,可以叫基于领域的 Web 测试(Domain Based Web Testing),具体来讲,就是 Page Object Pattern。

Page Object Pattern 里有四个基本概念:Driver、Page、Navigator 和 Shortcut 等。Driver 是测试真正的实现机制,比如 Selenium,比如 Watir,比如 HttpUnit。它们懂得如何去真正执行一个 Web 行为,通常包含像 Click、Select、Type 等这样的表示具体行为的方法;Page 是对一个具体页面的封装,它们了解页面的结构,知道诸如 id、name、class 和 xpath 这类实现细节,并描述用户可以在其上进行何种操作;Navigator 则代表了 URL,表示一些不经页面操作的直接跳转;最后 Shortcut 就是 helper 方法了,需要看具体的需要而定。下面来看一个超级简单的例子——测试登录页面。

1. Page Object

假设我们使用一个单独的登录页面进行登录,那么可能会将登录的操作封装在一个名为 LoginPage 的 page object 里:

复制代码
class LoginPage
def initialize driver
@driver = driver
end
def login_as user
@driver.type 'id=...', user[:name]
@driver.type 'xpath=...', user[:password]
@driver.click 'name=...'
@driver.wait_for_page_to_load
end
end

login_as 是一个具有业务含义的页面行为。在 login_as 方法中,page object 负责通过依靠 id、xpath、name 等信息完成登录操作。在测试中,我们可以这样来使用这个 page object:

复制代码
page = LoginPage.new $selenium
page.login_as :name => 'xxx', :password => 'xxx'

不过既然用了 Ruby,总要用一些 ruby sugar 吧,我们定义一个 on 方法来表达页面操作的环境:

复制代码
def on page_type, &block
page = page_type.new $selenium
page.instance_eval &block if block_given?
end

之后我们就可以使用 page object 的类名常量和 block 描述在某个特定页面上操作了:

复制代码
on LoginPage do
login_as :name => 'xxx', :password => 'xxx'
end

除了行为方法之外,我们还需要在 page object 上定义一些获取页面信息的方法,比如获取登录页面的欢迎词的方法:

复制代码
def welcome_message
@driver.get_text 'xpath=...'
end

这样测试也可表达得更生动一些:

复制代码
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name => 'xxx', :password => 'xxx'
end

当你把所有的页面都用 Page Object 封装了之后,就有效地分离了测试和页面结构的耦合。在测试中,只需使用诸如 login_as 和 add_product_to_cart 这样的业务行为,而不必依靠像 id、name 等这些具体且易变的页面元素了。当这些页面元素发生变化时,只需修改相应的 page object 就可以了,而原有测试基本不需要太大或太多的改动。

2. Assertation

只有行为还构不成测试,我们还要判断行为结果,并进行一些断言。简单回顾一下上面的例子,会发现还有一些很重要的问题没有解决:我怎么判断登录成功了呢?我如何才能知道真的是处在登录页面了呢?如果我调用下面的代码会怎样呢?

复制代码
$selenium.open url_of_any_page_but_not_login
on LoginPage {...}

因此我们还需要向 page object 增加一些断言性方法。至少,每个页面都应该有一个方法用于判断是否真正地达到了这个页面,如果不处在这个页面中的话,就不能进行任何的业务行为。下面修改 LoginPage 使之包含这样一个方法:

复制代码
LoginPage.class_eval do
include Test::Unit::Asseration
def visible?
@driver.is_text_present(...) && @driver.get_location == ...
end
end

在 visible? 方法中,我们通过对一些特定的页面元素(比如 URL 地址,特定的 UI 结构或元素)进行判断,从而可以得之是否真正地处在某个页面上。而我们目前表达测试的基本结构是由 on 方法来完成,我们也就顺理成章地在 on 方法中增加一个断言,来判断是否真的处在某个页面上,如果不处在这个页面则不进行任何的业务操作:

复制代码
def on page_type, &block
page = page_type.new $selenium
assert page.visible?, "not on #{page_type}"
page.instance_eval &block if block_given?
page
end

这个方法神秘地返回了 page 对象,这里是一个比较取巧的技巧。实际上,我们只想利用 page != nil 这个事实来断言页面的流转,比如,下面的代码描述登录成功的页面流转过程:

复制代码
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as :name => 'xxx', :password => 'xxx'
end
assert on WelcomeRegisteredUserPage

除了这个基本断言之外,我们还可以定义一些业务相关的断言,比如在购物车页面里,我们可以定义一个判断购物车是否为空的断言:

复制代码
def cart_empty?
@driver.get_text('xpath=...') == 'Shopping Cart(0)'
end

需要注意的是,虽然我们在 page object 里引入了 Test::Unit::Asseration 模块,但是并没有在断言方法里使用任何 assert* 方法。这是因为,概念上来讲 page object 并不是测试。使之包含一些真正的断言,一则概念混乱,二则容易使 page object 变成针对某些场景的 test helper,不利于以后测试的维护,因此我们往往倾向于将断言方法实现为一个普通的返回值为 boolean 的方法。

3. Test Data

测试意图的体现不仅仅是在行为的描述上,同样还有测试数据,比如如下两段代码:

复制代码
on LoginPage do
login_as :name => 'userA', :password => 'password'
end
assert on WelcomeRegisteredUserPage
registered_user = {:name => 'userA', :password => 'password'}
on LoginPage do
login_as registered_user
end
assert on WelcomeRegisteredUserPage

测试的是同一个东西,但是显然第二个测试更好的体现了测试意图:使用一个已注册的用户登录,应该进入欢迎页面。我们看这个测试的时候,往往不会关心用户名啊密码啊具体是什么,我们关心它们表达了怎样的测试案例。我们可以通过 DataFixture 来实现这一点:

复制代码
module DataFixture
USER_A = {:name => 'userA', :password => 'password'}
USER_B = {:name => 'userB', :password => 'password'}
def get_user identifier
case identifier
when :registered then return USER_A
when :not_registered then return USER_B
end
end
end

在这里,我们将测试案例和具体数据做了一个对应:userA 是注册过的用户,而 userB 是没注册的用户。当有一天,我们需要将登录用户名改为邮箱的时候,只需要修改 DataFixture 模块就可以了,而不必修改相应的测试:

复制代码
include DataFixtureDat
user = get_user :registered
on LoginPage do
login_as user
end
assert on WelcomeRegisteredUserPage

当然,在更复杂的测试中,DataFixture 同样可以使用真实的数据库或是 Rails Fixture 来完成这样的对应,但是总体的目的就是使测试和测试数据有效性的耦合分离:

复制代码
def get_user identifier
case identifier
when :registered then return User.find '....'
end
end

4.Navigator

与界面元素类似,URL 也是一类易变且难以表达意图的元素,因此我们可以使用 Navigator 使之与测试解耦。具体做法和 Test Data 相似,这里就不赘述了,下面是一个例子:

复制代码
navigate_to detail_page_for @product
on ProductDetailPage do
....
end

5. Shortcut

前面我们已经有了一个很好的基础,将 Selenium 测试与各种脆弱且意图不明的元素分离开了,那么最后 shortcut 不过是在蛋糕上面最漂亮的奶油罢了——定义具有漂亮语法的 helper:

复制代码
def should_login_successfully user
on LoginPage do
assert_equal 'Welcome!', welcome_message
login_as user
end
assert on WelcomeRegisteredUserPage
end

然后是另外一个 magic 方法:

复制代码
def given identifer
words = identifier.to_s.split '_'
eval "get_#{words.last} :#{words[0..-2].join '_'}"
end

之前的测试就可以被改写为:

复制代码
def test_should_xxxx
should_login_successfully given :registered_user
end

这是一种结论性的 shortcut 描述,我们还可以有更 behaviour 的写法:

复制代码
def login_on page_type
on page_type do
assert_equal 'Welcome!', welcome_message
login_as @user
end
end
def login_successfully
on WelcomeRegisteredUserPage
end
def given identifer
words = identifier.to_s.split '_'
eval "@#{words.last} = get_#{words.last} :#{words[0..-2].join '_'}"
end

最后,测试就会变成类似验收条件的样子:

复制代码
def test_should_xxx
given :registered_user
login_on LoginPage
assert login_successfully
end

总之 shortcut 是一个无关好坏,只关乎想象力的东西,尽情挥洒 Ruby DSL 吧!

结论

Selenium 是一个让人又爱又恨的东西,错误地使用 Selenium 会给整个敏捷团队的开发节奏带来灾难性的影响。不过值得庆幸的是正确地使用 Selenium 的原则也是相当的简单:

  1. 通过将脆弱易变的页面元素和测试分离开,使得页面的变化不会对测试产生太大的影响。
  2. 明确指定测试数据的意图,不在测试用使用任何具体的数据。
  3. 尽一切可能,明确地表达出测试的意图,使测试易于理解。

当然,除了遵循这几个基本原则之外,使用 page object 或其他 domain based web testing 技术是个不错的选择。它们将会帮助你更容易地控制 Selenium 测试的规模,更好地平衡覆盖率和执行效率,从而更加有效地交付高质量的 Web 项目。

此文中涉及的都是我最近几周以来对 Selenium 测试进行重构时所采用的真实技术。感谢 Nick Drew 帮助我清晰地划分了 Driver、 Page、Nagivator 和 Shortcut 的层次关系,它们构成我整个实践的基石;感谢 Chris Leishman,在和他结对编程的过程中,他帮助我锤炼了 Ruby DSL;还有 Mark Ryall 和 Abhi,是他们第一次在项目中引入了 Test Data Fixture,使得所有人的工作都变得简单起来。


作者简介:徐昊,ThoughtWorks 咨询师和敏捷过程教练; BJUG (Beijing Java User Group)和 AgileChina 主要创始人之一;RSSer(Ruby,Smalltalk & Scheme)。目前主要致力于研究编译理论和推广 DSL(Domain Specified Language)在实际项目中的应用。他的博客地址是: http://www.blogjava.net/raimundox

2007-05-21 22:467302

评论

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

Dubbo 学习笔记(三) Spring Boot 整合 Dubbo(官方版)

U2647

Spring Boot dubbo 4月日更

大数据前置知识-服务器及磁盘

大数据技术指南

大数据 4月日更

SpringBoot Admin2.0 集成 Java 诊断神器 Arthas 实践

阿里巴巴云原生

Java 运维 云原生 中间件 Arthas

技术人如何调研和选型第三方 SDK?全文干货

融云 RongCloud

EGG Network阿凡提 公链EFTalk全球首创POTP二叉交叉共识机制

币圈那点事

聪明人的训练(十二)

Changing Lin

4月日更

css网页布局小结

Darren

CSS

阿里高级架构师纯手打832页Java全栈知识点笔记,吃透后成功七面上岸滴滴!

Java架构追梦

Java 阿里巴巴 架构 面试 成长笔记

企业如何做数字化转型?想要资产状况及时把控,它的作用至关重要!

一只数据鲸鱼

数字化 数据可视化 资产管理

攻击区块链网络的都有哪些方式方法

CECBC

区块链

OKR实践中的痛点(5):战略缺失怎么玩OKR?

大叔杨

团队管理 OKR 敏捷 敏捷绩效

你的数仓函数结果不稳定,可能是属性指定错了

华为云开发者联盟

函数 GaussDB(DWS) 函数属性 函数下推 易失性级别

区块链电子政务——不动产综合服务平台

电微13828808271

装双系统?不需要!教你在iMac上流畅使用Windows

懒得勤快

Mac 虚拟机 苹果 crossover

大意!6行代码,“报废”5片单片机!

不脱发的程序猿

程序人生 嵌入式软件 单片机 4月日更 国产MCU

云小课 | 不了解EIP带宽计费规则?看这里!

华为云开发者联盟

带宽 弹性公网IP 带宽变更 计费模式

将本地maven仓库的数据恢复到Nexus仓库

白粥

工作笔记

云数据库时代的新思考,这位90后大咖想邀你聊聊

华为云开发者联盟

数据库 开源 opengauss GaussDB 华为云数据库

systemctl的使用

箭上有毒

linux运维 4月日更

面试官:请说说什么是BFC?大白话讲清楚

蛙人

CSS 大前端

程序员去大公司面试,我的头条面试经历分享,搞懂这些直接来阿里入职

欢喜学安卓

android 程序员 面试 移动开发

极智网络告警关联规则挖掘

鲸品堂

方法论 解决方案

“区块链+电子商务”,电商能否再创辉煌?

电微13828808271

进来看看是不是你想要的效果,Android吸顶效果,并有着ViewPager左右切换

第三女神程忆难

Java android kotlin 安卓 移动开发

NA(Nirvana)公链“为应用而生” NAC公链领跑公链新格局!

区块链第一资讯

Redis单线程已经很快,为何6.0要引入多线程?有啥优势?

Java架构师迁哥

一文读懂容器存储接口 CSI

阿里巴巴云原生

容器 云原生 k8s 存储 调度

26天吃透算法笔记,面试字节,面试官朝我比了个“ok”

比伯

Java 编程 架构 算法 技术宅

android开发面试题,字节跳动Android三面凉凉,手慢无

欢喜学安卓

android 程序员 面试 移动开发

MySQL 事务隔离

Sakura

4月日更

【LeetCode】子集二Java题解

Albert

算法 LeetCode 4月日更

应用Selenium和Ruby进行面向领域的Web测试_研发效能_徐昊_InfoQ精选文章