在 Ruby on Rails 中进行单元测试第二部分(转发)

上一篇 / 下一篇  2010-07-14 17:06:20 / 个人分类:Ruby

转发:http://soft.zdnet.com.cn/software_zone/2007/0901/482327.shtml


关于本系列

跨越边界系列中,作者 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 的功能测 试框架如何只通过一条 HTTPput命令访问 Web 页面。



分解功能测试

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

Rails 的每个功能测 试用例都要进行 HTTPputget。它们调用控制器的动作;控制器访问模型和 视图,并呈现 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命令执行了一个简单的 HTTPget。然后,测 试用例运行了三个断言:

  • 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, 它使用了一个 HTTPput。请将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 中的命令,通过 HTTPget发出列表动作:


清单 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 "#/../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 "#/../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 的建模策略。您将看到如何将代码生成用于动态语言。


TAG:

 

评分:0

我来说两句

Open Toolbar