跨越边界: 在集成框架中进行测试,第 2 部分

在 Ruby on Rails 中进行单元测试

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送



级别: 中级

Bruce Tate (bruce.tate@j2life.com), 总裁, RapidRed

2006 年 7 月 10 日

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

超越单元测试的扩展

关于本系列

跨越边界 系列中,作者 Bruce Tate 提出了这样一个观点:如今的 Java 程序员可以通过学习其他方法和语言得到很好的其他思路。自从 Java 明显成为所有开发项目的最佳选择以来编程前景已经改变。其他的框架正影响构建 Java 框架的方式,从其他语言学到的概念可以影响您的 Java 编程。您编写的 Python(或 Ruby、Smalltalk ... )代码可以改变您处理 Java 编码的方式。

本系列为您介绍与 Java 开发根本不同,但也可以直接应用于 Java 开发的编程概念和技术。在一些例子中,需要对技术进行集成以利用它。在另外一些例子中,您将能够直接应用这些概念。单独的工具不及其他语言和框架能够影响 Java 社区中的开发人员、框架甚至基本方法的思想那么重要。

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

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

运行测试

如果还没有阅读 第 1 部分,那么请先阅读它。然后,如果想跟随这篇文章一起编写代码,那么请确保您已经获得一个可工作的 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) 命令把 namedifficultydescription 方法发送给 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 putget。它们调用控制器的动作;控制器访问模型和视图,并呈现 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 返回代码的整数。请参阅 参考资料,获得 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 对象的 putget 方法,就可以访问应用程序的 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(一条路线)。您可以继续运行 postget


清单 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 可以为轻量级的、数据库支持的应用程序提供一些真正的价值。

在这个系列的下一篇文章中,我将不再介绍 Rails,而是查看基于 Web 的建模策略。您将看到如何将代码生成用于动态语言。



参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文

  • 超越 Java(O'Reilly,2005):本文作者编写的一本书,讲述 Java 语言的提高和稳定发展,以及在某些方面能够挑战 Java 平台的技术。

  • HTTP 1.1 Status Code Definitions:提供关于 HTTP 标准状态码的更多信息。

  • Running your Rails App Headless”:关于 Rails 集成测试框架的最佳讨论之一。

  • 用 Selenium 自动化验收测试”(Christian Hellsten,developerWorks,2005 年 12 月):学习如何在使用 Ruby on Rails 和 Ajax 的实际项目中用 Selenium 框架进行功能测试。

  • Book review: Agile Web Development with Rails”(Darren Torpey,developerWorks,2005 年 5 月):这些书可以加深读者对 Rails 和敏捷开发方法背后的逻辑的理解。

  • 测试优先 Ruby 编程”(Pat Eyler,developerWorks,2005 年 5 月):构建了一个符合测试优先原则的简单 Ruby 应用程序。

  • Java To Ruby: Things Your Manager Should Know(Pragmatic Bookshelf,2006):本文作者编写的一本书,讲述何时何处从 Java 编程转变到 Ruby on Rails,以及如何完成这种转变。

  • Programming Ruby (Pragmatic Bookshelf,2005):关于 Ruby 编程的一本流行书籍。

  • Java 技术专区:数百篇关于 Java 编程的各个方面的文章。

获得产品和技术
  • Ruby on Rails:下载开放源码的 Ruby on Rails Web 框架。

  • Ruby:从 Ruby 项目的 Web 站点得到它。


讨论


关于作者

Bruce Tate 居住在德克萨斯州的首府奥斯汀,他是一位父亲,同时也是山地车手和皮艇手。他是三本 Java 畅销书的作者,包括荣获 Jolt 大奖的 Better, Faster, Lighter Java。最近他又出版了 Beyond Java. 一书。他在 IBM 工作了 13 年,现在是 RapidRed 顾问公司 的创始人,在这里他潜心研究基于 Java 技术和 Ruby on Rails 的轻量级开发策略和架构

::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

订阅 substack 体验古早写作:


关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::