简介
对 Web 应用程序来讲,自动化的集成测试是一个非常重要的部分, 然而由于这些测试用例太依赖具体的 Web 页面的实现细节,这就给编写和维护带来的很大的挑战。 通常来讲有两种方法可以生成 Web 应用程序测试用例。
手工编写脚本:测试人员需要知道 Web 页面上有哪些表单、输入框、选择框、按钮等,以及这些表单元素的名称,ID 等属性,然后才能利用一些工具来编写测试用例。
通过工具录制生成:比如 IBM Rational Functional Tester 就提供了录制用户在 Web 界面的操作,自动生成测试用例的功能。
方法 1 需要测试人员了解太多的 Web 页面细节,这就使得测试人员不能把精力集中在业务逻辑上,一旦 Web 页面发生变化,将不得不花费大量精力更新脚本。方法 2 能够自动生成测试脚本,但是这些脚本的可读性很差,导致很难维护。同样如果 Web 页面发生变化,测试人员也需要重新录制所有的脚本。
那么有没有办法克服上述问题,让工作更加轻松一点呢?答案是肯定的!
例如一个在线的电子书店,对于用户购书的场景,我们可以用下面的脚本来进行集成测试 :
login 'test@test.com','pass4you' // 登录
list_books // 列出书籍
add_to_shop_cart '谁说大象不能跳舞' // 把《谁说大象不能跳舞》这本书加入到购物车中
读者可以看到, "login" , "list_books", "add_to_shop_cart" 这些术语已经完全脱离了具体的页面细节,将不会受到页面变化的影响, 它们是完全面向业务的,准确的体现了应用的业务逻辑,容易理解、易于维护,并且还能拿来和业务人员进行交流,甚至业务人员自己都能编写测试脚本。 有这么多的优点,那么如何实现它们呢?这正是本文要介绍的重点:利用动态语言 Ruby 来实现“业务驱动”的 Web 应用测试。
Ruby 介绍
Ruby,中文意思为红宝石,但是在计算机领域,它代表一种相当优秀的面向对象的脚本程序语言。它诞生于 1993 年,近年来随着 Ruby on Rails 这个“Killer application”在 Web 开发领域迅速蹿红。Ruby 在最初设计时吸收了很多别的语言的精华,例如 perl 语言的文本处理能力,Python 语言的简单性和可读性,以及方便的扩展能力和强大的可移植能力,Smalltalk 语言的纯面向对象语法思想,这就使它具备了很多其他语言的优点。Ruby 的设计理念是尽量减少编程时不必要的琐碎工作,让程序员在完成任务的同时充分的享受编程的乐趣。
Ruby 的特点如下:
面向对象:在 Ruby 中,一切皆是对象,包括其他语言中的基本数据类型,比如整数。
例如在 Java 中,对一个数求绝对值用 Math.abs(-20), 但在 Ruby 中一切皆对象,-20 这个数也是对象,所以可以这么做 -20.abs , 是不是更加形象和直观?
解释型脚本语言:无需编译,直接执行,开发周期短,调试方便。
动态性:已经定义的类可以在运行时修改。
本文的重点不是介绍 Ruby 语言本身,有兴趣的读者可以参见 参考资源 部分。
案例分析
51book
为了展示如何使用 Ruby 进行业务驱动的测试,同时又不让读者陷入到过多细节中,本文假想了一个简单的在线购书应用 ( 简称 51book),这个应用支持如下主要功能:
1.登录 : 用户必须登录才能购买书籍。
图 1. 登录
2.浏览书籍:包括按标题搜索书籍。
图 2. 浏览和搜索书籍
3.把书籍添加到购物车中,参见 图 2 中的“Add to cart”链接。
4.改变购物车中书籍的数量,并且重新计算。
业务操作
通过上面的介绍,读者应该对 51book 有了一个简单的了解,接下来我们考虑如何进行业务驱动的测试,首先需要定义面向业务的操作,这样才能在测试用例中使用它们。 简单起见,我们定义如下业务操作:
表 1. 业务操作
领域专用语言 (Domain Specific Language)
所谓领域专用语言(domain specific language / DSL),其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题, 而是专门针对某一特定问题的计算机语言。正如它的名称所宣称的那样,这种语言并不是通用的,只是专注于某个特定的“领域”, 例如 SQL 语言就是数据库的 DSL,使用 SQL 可以完成各种各样数据的操作,而不用关心底层的具体数据库实现。由于“领域专用”,你想用 SQL 来开发一个桌面应用程序是不可能的。
我们在上一节定义的 login , add_to_shop_cart , change_quantity 就是针对 51book 在线书店的 DSL。
Martin Fowler 把 DSL 分为两大类:外部 DSL 和内部 DSL。对外部 DSL 来讲,构建它需要做的是:(1) 定义面向领域的全新的语法。(2) 用某种语言编写解释器或编译器 ,由于这种语言是全新的,我们有很多工作需要做;那么对于内部 DSL 来说,我们可以选定一种灵活的语言,选取它一个语法的子集,并且利用这种语言的动态特性进行定制,这样就避免了重新打造一个全新语言的庞大工作量。
Ruby 语言具备非常丰富的语法和异常灵活的动态特征,非常适合创建动态 DSL。本文就是利用 Ruby 来创建 51book 面向测试的 DSL。
用 Ruby DSL 实现业务操作
原理
由于 Ruby 是一种动态脚本语言,是解释执行的,它提供了对一段文本进行 “evaluate”执行的方法。也就是说,我们可以提供一段文本(不必是完整的程序),Ruby 就可以在一个特定的上下文中执行它,当然这段文本需要符合 Ruby 的语法。
比如我们有一个文件 bookshop.txt,它包含了如下文本 : login "andy", "pass4you" , 那么怎么执行它呢?首先需要一个上下文,我们可以定义一个类来表示:
清单 1. BookshopDSLBuilder
1
2
3
4
5
6
7
8
9
10
|
class BookshopDSLBuilder def self .execute( dsl) builder= new builder.instance_eval( File .read(dsl), dsl) end def login(user= nil ,pwd= nil ) print user print pwd end end |
上面的代码非常简单,需要关注的是静态方法 execute, 当把 bookshop.txt 作为参数来调用它时,会有什么情况发生呢 ? 聪明的读者可能已经猜到了,那就是 user 和 pwd 的值会被打印出来。这段代码展示了 Ruby 语言的两个重要特点 :
instance_eval 方法会把一段文本当做代码来执行。执行的上下文就是对象 BookshopDSLBuilder。 所以当它碰到文本 "login" 时,会自动调用真正的方法 login。
在调用一个方法时,可以不加括号。这就是为什么 Ruby 会把文本 login "andy","pass4you" 当做一个方法调用的原因。
这两个特点就给我们搭了一座“桥”,使得我们可以把那个面向业务测试的文本诸如“login”,“add_to_cart”,“search_book”等转化为对特定方法的调用了。我们就可以在这些方法中实现某些逻辑。
Watir
我们现在已经能够把业务测试的脚本和 Ruby 的对象 / 方法连接起来,可是还需要第二座桥把 Ruby 和 Web 应用程序连接起来,这样才能使业务测试的脚本驱动 Web 页面进行测试。我们希望能有一个软件或工具可以像人一样来驱动浏览器的操作,例如点击链接,填充表单,点击按钮等等。当然它也可以检查页面的结果,例如期待的文本是否出现等。
开源工具 Watir 就是这样一个工具,除了具备上述功能外,它和 Ruby 语言还能进行无缝的集成,并且对浏览器尤其是 IE 有超强的控制能力。所以我们选取它作为第二座桥。
下面是一个使用 watir 的简单例子,它进入 Google 的首页,在搜索框中键入 "bookshop", 然后点击"搜索"按钮。 Watir 充分继承了 Ruby 语言简单明了的特点,读者可以看到使用 Watir 的脚本是相当直观,相当容易的。
清单 2. Watir 例子
1
2
3
4
5
|
require "watir" ie = Watir:: IE . new ie.goto "http://www.google.com" ie.text_field( :name , "q" ).set "bookshop" ie.button( :name , "btnG" ).click |
实现 Login
有了上面的两座“桥”,具体的实现就简单多了,对于每一个业务操作,我们需要做的是 :
(1) 在一个 Ruby 对象中 (BookshopDSLBuilder) 实现一个同名的方法
(2) 在方法实现中,利用 watir 来操作界面元素。当然前提是我们需要知道界面上有哪些元素。
先来看一看 Login 的实现:
清单 3. Login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class BookshopDSLBuilder include Test::Unit::Assertions #include ruby unit 的 Assertion def self .execute( dsl) builder= new builder.instance_eval( File .read(dsl), dsl) builder end def initialize @login_url = 'http://localhost:3000/bookshop/login' #51Book 的入口 #creat a ie instance @ie = Watir:: IE . new # 创建一个 Watir 的实例 end def login(user= nil ,pwd= nil ) @ie .goto @login_url @ie .text_field( :id , "user_name" ).set(user) # 设置用户名 @ie .text_field( :id , "user_password" ).set(pwd) # 设置密码 @ie .button( :type , "submit" ).click # 点击提交按钮 end end |
实现 add_to_shop_cart
把书籍添加的购物车中这个操作相对复杂,因为它接收的参数是一个书籍的标题,而在界面上"Add to Cart"却是一个只包含 book id, 不包含标题的链接,所以无法直接定位。
清单 4. Add to Cart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
< table width = '100%' class = 'book' > < tr > < td >title:</ td > < td >Agile development</ td > # 标题在这里 </ tr > < tr > < td >description:</ td > < td >The book of agile development</ td > </ tr > < tr > < td >price:</ td > < td >30.0</ td > </ tr > < tr > < td colspan = "2" > #Add_To_Cart Link 却在这里 < a href = '/bookshop/add_to_cart/1' >Add to Cart</ a > </ td > </ tr > </ table > |
这种情况下就可以利用 Watir 对 xpath 强大的支持,先找到标题,在从标题找到链接,最后点击链接即可。
清单 5. 使用 XPath
1
2
3
4
5
6
7
8
|
def add_to_cart(title) table = @ie .table( :xpath , "//table[@class='book']/tbody/tr/td[text()='" +title+ "']/../../../" ) if table[ 1 ][ 2 ].text == title href = table[ 4 ][ 1 ].links[ 1 ].href @ie .link( :href ,href).click end end |
对于其他的业务操作,具体的实现方式也是大同小异,这里不再一一介绍,有兴趣的读者可以参见 附件 中的代码,最后我们来看一个面向业务的 Web 页面测试例子:
清单 6. 一个完整的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
login 'andy' , 'pass4you' add_to_cart 'Agile development' add_to_cart 'Savor Blue' add_to_cart 'Programming Ruby' change_quantity 'Agile development' , 10 change_quantity 'Savor Blue' , 10 change_quantity 'Programming Ruby' , 10 recalculate_cart assert_total_price_is 900 search_book 'Ant cookbook' add_to_cart 'Ant cookbook' assert_total_price_is 910 |
总结
到目前为止,我们已经通过 Ruby 完整的实现了“业务驱动” 的 Web 应用测试,实际上我们通过 Ruby 实现了一个面向业务的抽象层,利用 Watir 把业务操作映射到了对 Html 页面的操作。这样当 Html 页面发生了变化的时候,只需要调整映射,而不需要更改业务层的操作。同时由于它们是完全面向业务的,就使得开发人员或测试人员能把精力集中到业务逻辑的测试上,而不用陷入实现的细节。
掌握了该方法以后,读者可以应用到自己的程序中,可以使得自己的测试编写简单,容易理解,易于维护。将会极大的提供 Web 应用的测试效率。