扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
作者:Bruce Tate 来源:51CTO.com 2007年9月1日
关键字: Ruby on Rails 单元 测试
|
在这由两部分组成的迷你系列的 第 1 部分 中,了解了如何用动态语言促进单元测试。本文将展示集成环境在功能测试和集成测试中的优势。单元测试包括对小的代码片断(例如方法)的测试,而且经常要把它们与周围的元素隔离开。功能测试和集成测试所测试的应用程序部分越来越多。功能测试用于测试单一特性(通常涉及一个接口)、执行任务的业务代码,以及与中间件服务交互的代码(例如数据库)。集成测试用于测试应用程序的多个不同特性。(功能测试在不太严谨的情况下通常也被称为集成测试。)
Java 开发人员在解决单元测试问题上已经获得了令人注目的成果,但在集成测试上则没有带来太多令人兴奋的消息。多数 Java 测试框架(如 JUnit 或 TestNG)主要侧重于单元测试。Java 编程中缺乏集成测试框架的一个原因是缺乏集中的架构或开发哲学。在后面的小节中,我将继续使用 Ruby on Rails 示例,这次的重点放在功能测试和新的 Rails 集成测试框架上。您将看到,在使用集成测试框架时,进行测试要容易得多。
如果还没有阅读 第 1 部分,那么请先阅读它。然后,如果想跟随这篇文章一起编写代码,那么请确保您已经获得一个可工作的 Rails 应用程序。在第 1 部分中,实现了一个简单的单元测试和几个 fixture。如果您跟随第 1 部分一起编写了代码,但是记不清是否使应用程序处于工作状态,那么您可以利用测试用例,先切换到项目目录,然后运行 rake
即可。清单 1 显示了我的结果:
> 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 所示的结果:
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 下的不同目录中。这个应用程序包含:
list
)
要了解这些是如何组合在一起的,请参见 trails_controller.rb 中的 list
方法,如清单 3 所示:
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 所示:
<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 做了以下工作:
<%
和 %>
之间的代码段(被称为语句),并用代码段的执行输出替代这一部分。语句可能不存在。<%=
和 %>
之间的代码段(被称为表达式),并用代码段返回的值替代这一部分。在有了模板策略之后,现在再来看一下 清单 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 显示了结果:
正如所期望的那样,可以看到一个包含每条路线的名称、说明和难度的表。接下来,我将介绍 Rails 的功能测试框架如何只通过一条 HTTP put
命令访问 Web 页面。
回忆一下就可以知道,Rails 单元测试只处理模型。Rails 中的功能测试调用 Web 页面,然后检查结果,从上到下地测试某一特性(包括模型、视图和管制器)。这种级别的集成测试很重要,因为可以确保系统的主要元素之间的交互与您对所提供的每个特性的预期一样。
Rails 的每个功能测试用例都要进行 HTTP put
和 get
。它们调用控制器的动作;控制器访问模型和视图,并呈现 Web 页面和结果。要获得详细的工作示例,请参见 Rails 在框架中生成的测试用例:
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 所示:
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
发出列表动作:
> 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
:
>> 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 那样:
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
运行测试用例,也可以单独运行每个测试用例。通过以一致的方式使用控制台和集成框架,可以尝试应用程序的各个方面,获得控制台中的结果,并用这些结果在自动测试用例中提供您的断言。
现在可以开始查看集成框架中的集成测试有什么不同了。对于这个示例,可以使用 fixture,它们在集成测试框架中工作。断言和表示想法的方式(例如请求和响应)都有统一的形式。
基本 Ruby 语言中的某些功能让 Rails 的测试更强大。可以使用 Ruby 做类似 mock 和存根所做的事。在编写这篇文章时,我正在使用 Rails 进行一些自动集成测试。我有一个依赖于当前日期的类。我只是打开了用于 Date
的现有 Ruby 类,并重新定义了 today
方法,让它返回 Date.civil(2, 2, 2006)
,如清单 10 所示:
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 的建模策略。您将看到如何将代码生成用于动态语言。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者