homeASCIIcasts

187: Testing Exceptions  (view original Railscast)

Other translations: En

Written by athrun (blog.csdn.net/shiweijian1986)

可能你最不想看到的一件事情是在你的Rails应用程序产品里有个错误提示500。当你的应用程序代码出现一个异常时这些情况经常会出现。

The 500 error on our site.

错误通知的几种方式

有很多解决方案可以让Rails应用程序在出现错误后立即通知我们,这里我们将要主要讨论四种解决方案。

第一种也是最经典的解决方法,异常通知(exception notification)插件.这是一个完整的基本的解决方案。当安装并且配置好它后,无论何时我们的应用程序出现错误,他都会发一封电子邮件给我们。如果我们的应用程序经常出现问题,那么你的邮箱里将会塞满各种报错的邮件,不过这也能激励我们解决这些错误。

第二种选择是异常记录(exception logger)插件,它在104章中被详细介绍过。它和异常通知(exception notification)的区别在于,它并不会发送电子邮件,它在一张数据库表中记录下错误信息,并且把这些错误信息在用户界面上反映出来。

另外两种选择都是收费性质的解决方案。第一种是蟾蜍(Hoptoad),它储存这些异常错误信息在他们的网页服务器上,你可以通过一个很友好的界面去连接这些服务器来取得这些信息。

The Hoptoad application.

最后一种选择是溢出处理专家(Exceptional),这是你可能会去尝试的另外一种非常好的解决方案

The Exceptional application.

调试我们的应用程序

所以,我们在我们的应用程序里安装溢出通知(exception notification)插件,并且利用它在邮箱里接受报错邮件。首先我们看下代码:

  A NoMethodError occurred in ProductsController#create:
 
  undefined method `add_to' for #<ActiveRecord::Errors:0x2480bdc>
  [RAILS_ROOT]/app/models/product.rb:6:in `validate'
 
-------------------------------
Request:
-------------------------------
 
  * URL: http://localhost:3000/products
  * IP Address: 127.0.0.1
  * Parameters: {"commit"=>"Submit", "authenticity_token"=>"f8hYo4S/c6iJaTV9wVj6E0BjWyy1soIBQtMUbRB8Ms0=", "product"=>{"name"=>"Headphones", "price"=>"-2"}, "action"=>"create", "controller"=>"products"}
  * Rails root: /Users/eifion/apps_for_asciicasts/ep187/store
 
-------------------------------
Session:
-------------------------------
 
  * session id: "0956b27849e48cdbc6de94b89cf5cf14"
  * data: {"flash"=>{}}

在这个通知中它指出我们在我们产品的模块的第六行中有一个方法没有定义。这个错误看起来是相当的简单,我们可以立即动手去解决它。但是,在我们做这个之前,我们要写一份测试缺陷计划。如果你在测试你的应用程序中并没有完全正确,你可能会想这是另外一个需要仔细测试的地方并且会考虑不再继续查看代码并暂时放下这个错误。即使你并不在测试你的应用程序,你也应该写一份覆盖这些错误的集成测试计划。我们过一会将要展示在这种给你最大帮助下的测试方式下写集成测试计划,执行集成测试一点也不困难。

即使你在测试驱动开发模式,你应该会对所有的需求点写集成测试计划。这个被报知的错误产生原因是在需求管理上对产品做的测试存在缺口,并且从这个缺口中漏掉了,所以我们可以很明确的找到那部分没有被很好测试的代码块。

首先一件我们需要做的事情是我们写一份集成测试缺陷计划来覆盖出问题的部分。我们将

要用Rails'内嵌的集成测试,但是我们可以用任何方式来进行集成测试,比如黄瓜切片器(Cucumber),来做这个。

我们可以针对集成测试生成一个文件,用这个文件,我们只需要以下代码就能调出错误

script/generate integration_test exceptions

这将给我们一个能放覆盖错误的测试的地方。我们将要用可以覆盖错误的测试去替代默认文件中放默认测试的地方。

require 'test_helper'  

class ExceptionsTest < ActionController::IntegrationTest   
  fixtures :all  
  
  test "POST /products" do  
    post "/products", "commit" => "submit", "product" =>{"name"=>"Headphones", "price" => "-2"}   
    assert_response :success  
  end  
end 

我们会对我们的测试名字进行统一命名规则即用我们请求的HTTP和URL的方法名进行组合。由于这一溢出发生在我们创建一个新的产品时,我们将要发送到/products目录下。如果我们看着这个通知里申请的具体。尽管我们可以删除该controlleractionauthenticity_token参数,如果我们看看在通知上请求的细节,我们将看到上面有些参数被传递,我们将需要传递其中的一些来模拟在我们的测试当中的请求。

运行集成测试只要运行如下代码:

rake test:integration

在这个测试的输出中我们将会看到以下错误:

1) Failure:
test_POST_/products(ExceptionsTest) [/test/integration/exceptions_test.rb:9]:
Expected response to be a <:success>, but was <500>
<"undefined method `add_to' for #<ActiveRecord::Errors:0x1036bc4f8>">
 
1 tests, 1 assertions, 1 failures, 0 errors

这个表示在溢出通知上我们可以看到相同的错误:一个没有定义 add_to方法的错误500。我们现在有一个重现溢出发生的测试。

默认情况下载集成测试失败时没有栈跟踪显示。这是基于Railscast的意见,但是对于这块的实现上这是一块非常好的代码。正因为这个地方,我们得到关于这个错误和它发生的地方的更多的信息。

1) Failure:
test_POST_/products(ExceptionsTest) [/test/integration/exceptions_test.rb:22]:
app/models/product.rb:6:in `validate'
app/controllers/products_controller.rb:16:in `create'
/test/integration/exceptions_test.rb:21:in `test_POST_/products'.
Expected response to be a <:success>, but was <500>
<"undefined method `add_to' for #<ActiveRecord::Errors:0x1036bc188>">

这个给我们足够的关于这个错误在哪里发生的信息和定位的信息。

 
class Product < ActiveRecord::Base   
  belongs_to :category  
  
  def validate   
    if price < 0   
      errors.add_to :price, "cannot be negative"  
    end  
  end  
end  

这个当然是应该添加add_to因此我们应该换成这个并且重新运行我们的测试。如果我们在对我们的程序进行单元测试,这时我们当然应该在修改这个问题之前写一个缺陷单元测试区覆盖这个特殊的功能点。

当我们再次运行我们的集成测试时,这个测试通过了。

 
$ rake test:integration
(in /Users/eifion/rails/apps_for_asciicasts/ep187/store)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I"lib:test" "/Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/integration/exceptions_test.rb" 
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
.
Finished in 0.188851 seconds.
 
1 tests, 1 assertions, 0 failures, 0 errors

现在这个测试通过了我们可以快速的尝试在浏览器中创建一个新的产品来检查我们的应用程序是正常工作并且问题已经被解决了。

另外一个例子

我们现在来看一下另外个例子。现在我们在产品控制者的编辑模板里有一个问题。

A ActionView::TemplateError occurred in ProductsController#edit:
 
  undefined method `add_to' for #<ActiveRecord::Errors:0x2480bdc>
  [RAILS_ROOT]/app/views/products/edit.html.erb:6: syntax error, unexpected tIVAR, expecting ')'
...ncat(( link_to "Show" @product ).to_s); @output_buffer.conca...
                              ^) on line #6 of app/views/products/edit.html.erb:
    3: <%= render :partial => 'form' %>
    4: 
    5: <p>
    6:   <%= link_to "Show" @product %> |
    7:   <%= link_to "View All", products_path %>
    8: </p>
 
        app/views/products/edit.html.erb:12:in `compile!'
 
 
-------------------------------
Request:
-------------------------------
 
  * URL: http://localhost:3000/products/8/edit
  * IP Address: 127.0.0.1
  * Parameters: {"action"=>"edit", "controller"=>"products", "id"=>"8"}
  * Rails root: /Users/rbates/code/railscasts-episodes/episode-187/store
 
-------------------------------
Session:
-------------------------------
 
  * session id: "0956b27849e48cdbc6de94b89cf5cf14"
  * data: {"flash"=>{}}

在我们开始写集成测试来覆盖这个缺陷之前。这一次失败的请求是GET,而不是一个POST我们不需要任何参数传递,只提出一个请求的URL和检查的反应是200。

test "GET /products/8/edit" do  
  get "/products/8/edit"  
  assert_response :success  
end

当我们运行这个集成测试,测试失败了,但是却不是那个我们预期的错误,而被一个错误404替代。

1) Failure:
test_GET_/products/8/edit(ExceptionsTest) [/test/integration/exceptions_test.rb:26]:
Expected response to be a <:success>, but was <404>

这样做的原因是,我们没有一个在我们的测试数据库中id为8的产品。这是一种编写集成测试的小伎俩:有时测试在某个结构被应用程序而定。在这种情况下,测试将不得不重新创建该结构来让一个我们测试的代码通过。这可能意味着session变量或数据库记录可能需要创建得到正确的应用程序状态。

在这个特定的测试中,我们必须有一个现有的产品。我们可以利用Factories 机制(正如我们回到158章),或使用加载数据。我们为简单起见,在我们的应用程序里加载数据,我们将使用并更改测试,以便它在调用创建该产品的编辑网页。

test "GET /products/8/edit" do  
  product = Product.first   
  get "/products/#{product.id}/edit"  
  assert_response :success  
end  

再次运行这个测试并且我们将看见这个预料中的错误。

1) Failure:
test_GET_/products/8/edit(ExceptionsTest) [/test/integration/exceptions_test.rb:27]:
Expected response to be a <:success>, but was <500>
<"compile error\n/Users/eifion/rails/apps_for_asciicasts/ep187/store/app/views/products/edit.html.erb:4: syntax error, unexpected tIVAR, expecting ')'\n...ncat(( link_to \"Show\" @product ).to_s); @output_buffer.conca...\n                              ^">

在我们的编辑视窗里这个错误看起来像一个语法错误。如果我们看这个代码然后我们可以看到我们在其中一个link_to调用中缺少了一个逗号。

<% title "Edit Product" %>   
<%= render :partial => 'form' %>   
<p>   
  <%= link_to "Show" @product %>   
  <%= link_to "View All", products_path %>   
</p>  

如果我们解决这个问题并且再次运行这个测试那他们都会通过因为我们在程序中已经把所有异常都解决了。一旦你习惯于这个工作方式这将带来一个有效的处理异常的提升你应用程序的方式。最后,如果你想提高你的应用程序的集成测试,那么它值得去看一看156章的Webrat。