在 Ruby on Rails 中进行单元测试 (二)
 
2009-01-15 作者: Bruce Tate 出处:developerWorks 中国
 

在 Ruby on Rails 中进行单元测试(一)中,介绍了如何用 Ruby on Rails 进行单元测试,并展示了如何利用这种方式的某些方面改进 Java™ 的单元测试。Java 开发人员对更高级测试的选择更加有限。本文中,将继续研究 Rails,体会用于功能测试和集成测试的集成框架的优势。

超越单元测试的扩展

本文将展示集成环境在功能测试和集成测试中的优势。单元测试包括对小的代码片断(例如方法)的测试,而且经常要把它们与周围的元素隔离开。功能测试和集成测试所测试的应用程序部分越来越多。功能测试用于测试单一特性(通常涉及一个接口)、执行任务的业务代码,以及与中间件服务交互的代码(例如数据库)。集成测试用于测试应用程序的多个不同特性。(功能测试在不太严谨的情况下通常也被称为集成测试。)

Java 开发人员在解决单元测试问题上已经获得了令人注目的成果,但在集成测试上则没有带来太多令人兴奋的消息。多数 Java 测试框架(如 JUnit 或 TestNG)主要侧重于单元测试。Java 编程中缺乏集成测试框架的一个原因是缺乏集中的架构或开发哲学。在后面的小节中,我将继续使用 Ruby on Rails 示例,这次的重点放在功能测试和新的 Rails 集成测试框架上。您将看到,在使用集成测试框架时,进行测试要容易得多。

运行测试

如果还没有阅读第一篇,那么请先阅读它。然后,如果想跟随这篇文章一起编写代码,那么请确保您已经获得一个可工作的 Rails 应用程序。在第 1 部分中,实现了一个简单的单元测试和几个 fixture。如果您跟随第 1 部分一起编写了代码,但是记不清是否使应用程序处于工作状态,那么您可以利用测试用例,先切换到项目目录,然后运行 rake 即可。清单 1 显示了我的结果:

清单 1. 用 rake 运行所有测试

> bruce-tates-computer:~/rails/trails batate$ rake
(in /Users/batate/rails/trails)
/usr/local/ror/bin/ruby -Ilib:test
   "/usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader.rb"
   "test/functional/trails_controller_test.rb"
Loaded suite /usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader
Started
EEEEEEEEEEEEEEEE
Finished in 0.070797 seconds.

  1) Error:
test_create(TrailsControllerTest):
Errno::ENOENT: No such file or directory - /tmp/mysql.sock
    /usr/local/ror/lib/ruby/gems/1.8/gems/activerecord-1.14.0/
      lib/active_record/vendor/mysql.rb:104:in 'initialize'
    /usr/local/ror/lib/ruby/gems/1.8/gems/activerecord-1.14.0/
      lib/active_record/vendor/mysql.rb:104:in 'real_connect'
    /usr/local/ror/lib/ruby/gems/1.8/gems/activerecord-1.14.0/
      lib/  active_record/connection_adapters/mysql_adapter.rb:331:in 'connect'
   
   
...results deleted...


8 tests, 0 assertions, 0 failures, 16 errors
/usr/local/ror/bin/ruby -Ilib:test "/usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/
   lib/rake/rake_test_loader.rb" 
rake aborted!
Test failures

(See full trace by running task with --trace)

可以看到有一些问题存在:rake 生成了 16 个错误。跟踪显示,Rails 无法建立连接。我忘记启动数据库引擎了。我将启动数据库引擎,然后再次运行 rake。这次我得到了清单 2 所示的结果:

清单 2. 在 rake 内通过测试

rake
(in /Users/batate/rails/trails)
/usr/local/ror/bin/ruby -Ilib:test
   "/usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader.rb"
   "test/unit/trail_test.rb"
Loaded suite /usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader
Started
...
Finished in 0.09541 seconds.

3 tests, 5 assertions, 0 failures, 0 errors
/usr/local/ror/bin/ruby -Ilib:test
   "/usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader.rb"
   "test/functional/trails_controller_test.rb"
Loaded suite /usr/local/ror/lib/ruby/gems/1.8/gems/rake-0.7.0/lib/rake/rake_test_loader
Started
........
Finished in 0.169756 seconds.

8 tests, 28 assertions, 0 failures, 0 errors

这样就好多了。测试正常运行,而我们准备构建更多测试用例。如果仔细查看清单 2 就会发现,rake 生成了两组结果。第一组(第 1 部分的单元测试)看起来应当熟悉。下一组是从框架中自动生成的功能测试。

控制器和视图快速入门

在查看测试代码之前,需要对 Rails 的用户界面层有更好的理解。在第 1 部分中,用 script/generate scaffold Trail Trails 生成框架代码时,Rails 根据数据库的内容为应用程序创建了一个控制器和系列视图。控制器的代码位于 app/controller/trails_controller.rb,视图则全部位于 app/views/trails 下的不同目录中。这个应用程序包含:

  •   默认 Web 页面实现,显示路线(trail)列表(叫做 list)
  •   路线的细节信息的显示页面
  •   路线的通用表单
  •   创建或编辑路线的页面

要了解这些是如何组合在一起的,请参见 trails_controller.rb 中的 list 方法,如清单 3 所示:

清单 3. app/controllers/trails_controller.rb 中的部分代码清单

def list
  @trail_pages, @trails = paginate :trails, :per_page => 10
end

传入的超文传输协议(HTTP)请求进入控制器。(HTTP 是支持浏览器、Rails 和所有基于浏览器的应用程序的底层协议)。在这篇文章后面,您将看到功能测试如何通过使用 HTTP 命令来调用功能测试用例。清单 3 的代码设置了 Rails 显示线路的分页列表时需要的实例变量。视图需要一个分页器对象,即 Rails 分配给 @trail_pages 的分页器对象,还需要 @trails 中的路线列表。默认情况下,Rails 使用与控制器方法相同的名称呈现视图。要查看视图,请参阅 app/views/trails/list.rhtml 中的表格定义,如清单 4 所示:

清单 4. list.rhtml 的部分代码清单

<table>
   <tr>
   <% for column in Trail.content_columns %>
      <th><%= column.human_name %></th>
   <% end %>
   </tr>

<% for trail in @trails %>
   <tr>
   <% for column in Trail.content_columns %>
      <td><%=h trail.send(column.name) %></td>
   <% end %>
      <td><%= link_to 'Show', :action => 'show', :id => trail %></td>
      <td><%= link_to 'Edit', :action => 'edit', :id => trail %></td>
      <td><%= link_to 'Destroy', { :action => 'destroy', :id => trail },
         :confirm => 'Are you sure?', :post => true %></td>
   </tr>
<% end %>
</table> 

Rails 中的视图策略是:创建一个简单字符串,然后做一些替换。这个策略叫做建模,它构成了大多数现代 Web 框架的基础,包括 Java 框架(例如 Tapestry、JavaServer Faces(JSF)、JavaServer Pages (JSP) 和 WebWork)。在这个示例中,Rails 做了以下工作:

1. 执行 <% 和 %> 之间的代码段(被称为语句),并用代码段的执行输出替代这一部分。语句可能不存在。
2. 执行 <%= 和 %> 之间的代码段(被称为表达式),并用代码段返回的值替代这一部分。
3. 处理布局、偏好、帮助程序以及其他类型的代码片断时。这些特性允许使用不同的复合部件构建复杂的 Web 页面。在这里,我就不对细节做过多介绍了。

在有了模板策略之后,现在再来看一下 清单 4。您可以看到访问活动记录 Trail 模型并用 <% for trail in @trails %> 命令在 @trails 中的每条路线上循环的 list.rhtml 视图。(您已经填充了控制器中的 @trails 实例变量)。对于每条路线,该视图都将得到 Trail.content_columns,它是 trails_development 数据库中 trails 表的列的列表。然后,该视图通过在列表中的每个列上进行循环,提供数据库中每一列的值。trail.send(column_name) 命令把 name、difficulty 和 description 方法发送给 trail。

现在是在屏幕上查看结果的时候了。如果回忆一下,应当记得您已经在第 1 部分的示例中键入了一些 fixture 形式的测试数据。要把它们加载到开发环境(fixture 默认装入测试环境)中,则只需键入 rake load_fixtures 即可。启动 Rails 服务器(在 Unix 上用 script/server,在 Windows 上用 ruby script/server),把浏览器指向 localhost:3000/trails/list 就可以看到结果。在这个 URL 中,trails 是控制器的名称,list 是动作的名称,由 list 控制器方法实现。图 1 显示了结果:

图1. 列出路线

正如所期望的那样,可以看到一个包含每条路线的名称、说明和难度的表。接下来,我将介绍 Rails 的功能测试框架如何只通过一条 HTTP put 命令访问 Web 页面。

分解功能测试

回忆一下就可以知道,Rails 单元测试只处理模型。Rails 中的功能测试调用 Web 页面,然后检查结果,从上到下地测试某一特性(包括模型、视图和管制器)。这种级别的集成测试很重要,因为可以确保系统的主要元素之间的交互与您对所提供的每个特性的预期一样。

Rails 的每个功能测试用例都要进行 HTTP put 和 get。它们调用控制器的动作;控制器访问模型和视图,并呈现 Web 页面和结果。要获得详细的工作示例,请参见 Rails 在框架中生成的测试用例:

清单 5. 来自 test/functional/trails_controller_test.rb 的 test_list

def test_list
  get :list

  assert_response :success
  assert_template 'list'

  assert_not_nil assigns(:trails)
end

清单 5 中的测试用例利用 get :list 命令执行了一个简单的 HTTP get。然后,测试用例运行了三个断言:

  •   assert_response :success:HTTP 命令成功完成。
  •   assert_template 'list':控制器动作呈现 list 模板。
  •   assert_not_nil assigns(:trails):控制器把 @trails 实例变量分配给一些非 null 的值。

使用单元测试框架,如果断言为 ture,没有错误出现,那么测试用例就通过;否则,测试用例失败。

test_list 测试用例可以声明 :success 响应,但是它应当声明 :redirect (代表 HTTP 重定向)、:missing (代表 not_found),或代表单个 HTTP 返回代码的整数。现在请看 test_create,它使用了一个 HTTP put。请将 test_create 更改成如清单 6 所示:

清单 6. 测试表单

def test_create
   num_trails = Trail.count

   post :create, :trail => {:name => "Hermosa Creek", :description =>
      "Lots of altitude, all down", :difficulty => "Medium"}

   assert_response :redirect
   assert_redirected_to :action => 'list'

   assert_equal num_trails + 1, Trail.count
end

trails_controller_test.rb 中自动生成的这个测试用例的版本包括 post :create, :trail => {},它调用 create 方法,空哈希表表示新路线。这个代码应当创建一条新路线,该路线有一个所有属性都为 null 的 Trail 对象。清单 6 修改了代码,以传递代表路线属性的哈希映射表。这个哈希映射表接口对于在测试框架中指定对象而言非常有用。然后,测试用例用 Trail 模型确保创建了新路线。

清单 5 和清单 6 中的测试用例不像第 1 部分中的单元测试那样处理每个细节。但是它们可以保证调用了业务逻辑,保证控制器逻辑没有检测到任何错误,并保证得到了正确的 HTTP 响应。

Rails 还提供了另一种测试用例:集成测试。

集成测试

功能测试用于测试单一特性,而集成测试可能触及许多不同的页面。例如,购物车单元测试可以测试出您可能通过模型 API 将一件商品添加到购物车中。购物车的功能测试可以确保您能够通过登录某一 Web 页面将商品添加到购物车中。而集成测试则可以保证能够登录、添加商品和结账。

在 “Running Your Rails App Headless”中,Mike Clark(Rails 社区领先的测试专家之一)详细介绍了集成测试框架。开始进行讨论时,他介绍了如何运行没有 Web 页面的(即 headless)应用程序。这项功能使得搜集编写集成测试的足够信息变得更容易。从 Rails 1.1 开始,可以直接从控制台调用控制器。不需要浏览器,只要调用 app 对象的 put 和 get 方法,就可以访问应用程序的 Web 页面。

请启动控制台,键入清单 7 中的命令,通过 HTTP get 发出列表动作:

清单 7. 从控制台使用 Rails 集成测试框架

> script/console Loading development environment.
>> app.class
=> ActionController::Integration::Session
>> app.get('trails', 'list')
=> 200
>> app.get("trails/list")
=> 200
>> app.response =~ /Barton Creek/
=> false
>> app.response =~ /Emma Long/
=> false
>> app.response.body =~ /Emma Long/
=> 331
>>

在清单 7 中,从控制台以两种形式发送请求,调用 trails 控制器的 list 动作。然后,通过与正则表达式 /Emma Long/ 匹配,可以看到生成的 HTML 页面中包含 Emma Long(一条路线)。您可以继续运行 post 和 get:

清单 8. 通过 post 实现删除

>> app.post("trails/destroy/1")
=> 302
>> Trail.find_all
=> [#<Trail:0x25a8e34 @attributes={"name"=>"Bear Creek", "id"=>"2",
   "description"=>"Too many downed trees.", "difficulty"=>"easy"}>]
>> Trail.find_all.size
=> 1
>> app.response.redirect_url
=> "http://www.example.com/trails/list"
>>

通过控制台集成测试 API,现在有了构建集成测试的足够信息。请使用 script/generate integration_test DestroyAndShow 生成一个集成测试,并将它编辑成清单 9 那样:

清单 9. test/integration/destroy_and_show.rb

require "#{File.dirname(__FILE__)}/../test_helper"

class DestroyAndShowTest < ActionController::IntegrationTest
  fixtures :trails

  def test_multiple_actions
    get "trails/list"
    assert_response :success
   
    post "trails/destroy/1"
    assert_response :redirect
    assert_nil(response.body =~ /Emma Long/)
    assert_equal(2, Trail.find_all.size)
   
    follow_redirect!   
    assert_response :success
   
   
    get "trails/show/2"
    assert_response :success
   
   
  end
end

这个示例使用的集成框架与前面通过 Rails 控制台使用的框架相同,使用的断言模型也与功能测试和单元测试框架的模型相同。可以用 rake 运行测试用例,也可以单独运行每个测试用例。通过以一致的方式使用控制台和集成框架,可以尝试应用程序的各个方面,获得控制台中的结果,并用这些结果在自动测试用例中提供您的断言。

在 Ruby 中测试与在 Java 语言中测试的对比

现在可以开始查看集成框架中的集成测试有什么不同了。对于这个示例,可以使用 fixture,它们在集成测试框架中工作。断言和表示想法的方式(例如请求和响应)都有统一的形式。

基本 Ruby 语言中的某些功能让 Rails 的测试更强大。可以使用 Ruby 做类似 mock 和存根所做的事。在编写这篇文章时,我正在使用 Rails 进行一些自动集成测试。我有一个依赖于当前日期的类。我只是打开了用于 Date 的现有 Ruby 类,并重新定义了 today 方法,让它返回 Date.civil(2, 2, 2006),如清单 10 所示:

清单 10. 用 Rails 创建存根

require "#{File.dirname(__FILE__)}/../test_helper"

 class Date
   def self.today
     return Date.civil(2006, 2, 2)
   end
 end

class NameOfTest ...continue test case here...

对于我的测试用例,我什么都不需要做。现在,不论测试用例什么时候运行,today 都会是美国的假日土拔鼠日。只使用了五行代码,我就有了一个可工作的存根。在这个示例中,这个 mock 对象只能用于测试用例。如果需要将这个 mock 对象用于多个测试用例,那么可以给这个 mock 对象添加测试和模拟的代码,并重新使用它。

总之,我对 Ruby 的测试体验的评价是:非常必要(因为动态语言容易出错的特性),并且更强大。其中部分力量来自通过 Rails 使得代码生成、断言、数据库支持,以及诊断工具无缝地在一起工作的集成体验。

但是 Java 技术确实有自己的优势。在将测试集成到开发环境方面它做得更好,它还有更好的持续集成工具。也可以找到模拟最常见企业特性的更多框架。Java 开发人员有另一个理论优势:他们可以在没有数据库支持的情况下,更容易地运行应用程序。没有数据库支持就测试 Rails 应用程序几乎没有意义,因为许多 Rails 值是通过元编程(metaprogramming)把 SQL 特性编织起来而得到的。所以,Java 测试套件通常运行得更快,因为套件中的测试用例不需要访问数据库。

如果使用 Java 代码生成,Rails 可以为您提供一些关于如何使用测试生成增强您的代码生成的好主意。如果正在补充自己的测试框架,那么 Rails 的测试 API 既简单又漂亮。如果对超越 Java 编程语言感兴趣,那么 Rails 可以为轻量级的、数据库支持的应用程序提供一些真正的价值。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织