自动化的 Web 集成测试对于保证软件质量是很有效的,但是编写和维护这些测试用例却不是一件容易的事。本文介绍的是一种利用 Ruby 实现业务驱动的集成测试方法, 通过该方法,读者能够脱离复杂的技术细节,把注意力集中到业务逻辑的测试中来。
对 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,中文意思为红宝石,但是在计算机领域,它代表一种相当优秀的面向对象的脚本程序语言。它诞生于 1993 年,近年来随着 Ruby on Rails 这个“Killer application”在 Web 开发领域迅速蹿红。Ruby 在最初设计时吸收了很多别的语言的精华,例如 perl 语言的文本处理能力,Python 语言的简单性和可读性,以及方便的扩展能力和强大的可移植能力,Smalltalk 语言的纯面向对象语法思想,这就使它具备了很多其他语言的优点。Ruby 的设计理念是尽量减少编程时不必要的琐碎工作,让程序员在完成任务的同时充分的享受编程的乐趣。
Ruby 的特点如下:
- 面向对象:在 Ruby 中,一切皆是对象,包括其他语言中的基本数据类型,比如整数。
例如在 Java 中,对一个数求绝对值用
Math.abs(-20)
, 但在 Ruby 中一切皆对象,-20 这个数也是对象,所以可以这么做-20.abs
, 是不是更加形象和直观? - 解释型脚本语言:无需编译,直接执行,开发周期短,调试方便。
- 动态性:已经定义的类可以在运行时修改。
本文的重点不是介绍 Ruby 语言本身,有兴趣的读者可以参见 参考资源 部分。
为了展示如何使用 Ruby 进行业务驱动的测试,同时又不让读者陷入到过多细节中,本文假想了一个简单的在线购书应用 ( 简称 51book),这个应用支持如下主要功能:
- 登录 : 用户必须登录才能购买书籍。
图 1. 登录
- 浏览书籍:包括按标题搜索书籍。
图 2. 浏览和搜索书籍
- 把书籍添加到购物车中,参见 图 2 中的“Add to cart”链接。
- 改变购物车中书籍的数量,并且重新计算。
图 3. 购物车
通过上面的介绍,读者应该对 51book 有了一个简单的了解,接下来我们考虑如何进行业务驱动的测试,首先需要定义面向业务的操作,这样才能在测试用例中使用它们。 简单起见,我们定义如下业务操作:
表 1. 业务操作
业务操作 | 功能说明 |
---|---|
login "user_name", "password" | 该操作接受用户名和密码,触发在 Web 界面的登录 |
add_to_shop_cart "book_title" | 该操作接受一个书籍名称为参数,可以把当前页面的该书籍加入到购物车中 |
search_book "book_title" | 该操作以书籍名称为参数来搜索书籍 |
change_quantity "book_title" | 该操作改变一本书在购物车中的数量 |
recalculate_cart "book_title" | 该操作在改变了购物车中的内容后,可以重新计算总价格 |
assert_total_price_is "price" | 该操作实际上是个断言 (Assert), 它被用来 Assert 购物车中的总价格和测试人员的期待是相符的 |
领域专用语言 (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 是一种动态脚本语言,是解释执行的,它提供了对一段文本进行 “evaluate
”执行的方法。也就是说,我们可以提供一段文本(不必是完整的程序),Ruby 就可以在一个特定的上下文中执行它,当然这段文本需要符合 Ruby 的语法。
比如我们有一个文件 bookshop.txt,它包含了如下文本 : login "andy", "pass4you" , 那么怎么执行它呢?首先需要一个上下文,我们可以定义一个类来表示:
清单 1. BookshopDSLBuilder
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 |
上面的代码非常简单,需要关注的是静态方法