使用Ruby实现简单的事物驱动的web应用的教程

716 查看

简介

对 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. 登录

2015415152602575.jpg (314×90)

    2.浏览书籍:包括按标题搜索书籍。
图 2. 浏览和搜索书籍

2015415152632077.jpg (567×387)

3.把书籍添加到购物车中,参见 图 2 中的“Add to cart”链接。
4.改变购物车中书籍的数量,并且重新计算。

2015415152701463.jpg (461×173)

业务操作

通过上面的介绍,读者应该对 51book 有了一个简单的了解,接下来我们考虑如何进行业务驱动的测试,首先需要定义面向业务的操作,这样才能在测试用例中使用它们。 简单起见,我们定义如下业务操作:
表 1. 业务操作

2015415152724682.jpg (958×273)

领域专用语言 (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

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 例子

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

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

 <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

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. 一个完整的例子

 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 应用的测试效率。