对Ruby on Rails进行高效的单元测试的教程_ruby专题

在笔者开发的系统中,有大量的数据需要分析,不仅要求数据分析准确,而且对速度也有一定的要求的。没有写测试代码之前,笔者用几个很大的方法来实现这种需求。结果可想而知,代码繁杂,维护困难,难于扩展。借业务调整的机会,笔者痛定思痛,决定从测试代码做起,并随着不断地学习和应用,慢慢体会到测试代码的好处。

  •     改变思路:能做到从需求到代码的过程转换,逐步细化;
  •     简化代码:力图让每个方法都很小,只专注一件事;
  •     优化代码:当测试代码写不出来,或者需要写很长的时候,说明代码是有问题的,是可以被分解的,需要进一步优化;
  •     便于扩展:当扩展新业务或修改旧业务时,如果测试代码没有成功,则说明扩展和修改不成功;
  •     时半功倍:貌似写测试代码很费时,实际在测试、部署和后续扩展中,测试代码将节省更多的时间。

环境搭建

笔者采用的测试环境是比较流行通用的框架:RSpec + Factory Girl,并用autotest自动工具。RSpec是一种描述性语言,通过可行的例子描述系统行为,非常容易上手,测试用例非常容易理解。Factory Girl可以很好的帮助构造测试数据,免去了自己写fixture的烦恼。Autotest能自动运行测试代码,随时检测测试代码的结果,并且有很多的插件支持,可以让测试结果显示的很炫。
第一步 安装rspec和rspec-rails

在命令行中执行如下命令:

$ sudo gem install rspec v = 1.3.0
$ sudo gem install rspec-rails v = 1.3.2

安装完成后,进入rails应用所在的目录,运行如下脚本,生成spec测试框架:

$ script/generate rspec
  exists lib/tasks
 identical lib/tasks/rspec.rake
 identical script/autospec
 identical script/spec
  exists spec
 identical spec/rcov.opts
 identical spec/spec.opts
 identical spec/spec_helper.rb

第二步 安装factory-girl

在命令行中执行如下命令:

$ sudo gem install rspec v = 1.3.0
$ sudo gem install rspec-rails v = 1.3.2

安装完成后,进入rails应用所在的目录,运行如下脚本,生成spec测试框架:

$ script/generate rspec
  exists lib/tasks
 identical lib/tasks/rspec.rake
 identical script/autospec
 identical script/spec
  exists spec
 identical spec/rcov.opts
 identical spec/spec.opts
 identical spec/spec_helper.rb

第二步 安装factory-girl

在命令行中执行如下命令:

$ sudo gem install factory-girl

在config/environment/test.rb中,加入factory-girl这个gem:

config.gem "factory_girl"

在spec/目录下,增加一个factories.rb的文件,用于所有预先定义的model工厂。
第三步 安装autotest

在命令行中执行如下命令:

$ sudo gem install ZenTest
$ sudo gem install autotest-rails

然后设置与RSpec的集成,在rails应用的目录下,运行如下的命令,就可以显示测试用例的运行结果。

RSPEC=true autotest or autospec

在自己的home目录下,增加一个.autotest设置所有的Rails应用的autotest插件。当然,也可以把这个文件加到每个应用的根目录下,这个文件将覆盖home目录下的文件设置。autotest的插件很多,笔者用到如下的plugin:

$ sudo gem install autotest-growl
$ sudo gem install autotest-fsevent
$ sudo gem install redgreen

设置.autotest文件,在.autotest中,加入如下代码。

require 'autotest/growl'
require 'autotest/fsevent'
require 'redgreen/autotest' 

Autotest.add_hook :initialize do |autotest|
 %w{.git .svn .hg .DS_Store ._* vendor tmp log doc}.each do |exception|
  autotest.add_exception(exception)
 end
end

测试经验

安装了必要的程序库以后,就可以写测试代码了。本例中,所有应用都是在Rails 2.3.4上开发的,RSpec采用的是1.3.0的版本。为了很好的说明问题,我们假定这样的需求:判断一个用户在一个时间段内是否迟到。写测试代码时都是遵循一个原则,只关心输入和输出,具体的实现并不在测试代码的考虑范围之内,是行为驱动开发。根据这个需求,我们将会设计方法absence_at(start_time,end_time),有两个输入值start_time和end_time以及一个输出值,类型是boolean。对应的测试代码如下:

describe "User absence or not during [start_time,end_time]" do
 before :each do
  @user = Factory(:user)
 end

 it "should return false when user not absence " do
  start_time = Time.utc(2010,11,9,12,0,0,0)
  end_time = Time.utc(2010,11,9,12,30,0)
  @user.absence_at(start_time,end_time).should be_false
 end

 it "should return true when user absence " do
  start_time = Time.utc(2010,11,9,13,0,0,0)
  end_time = Time.utc(2010,11,9,13,30,0)
  @user.absence_at(start_time,end_time).should be_ture
 end
end

测试代码已经完成。至于absence_at方法我们并不关心它的实现,只要这个方法的结果能让测试代码运行结果正确就可以。在此测试代码的基础上,就可以大胆地去完成代码,并根据测试代码的结果不断修改代码直到所有测试用例通过。
Stub的使用

写测试代码,最好首先从model开始。因为model的方法能很好与输入输出的原则吻合,容易上手。最初的时候,你会发现mock和stub很好用,任何的对象都可以mock,并且在它的基础上可以stub一些方法,省去构造数据的麻烦,一度让笔者觉得测试代码是如此美丽,一步步的深入,才发现自己陷入了stub的误区。还是引用上面的例子,我们的代码实现如下:

class User < ActiveRecord::Base
 def absence_at(start_time,end_time)
  return false if have_connection_or_review?(start_time,end_time)
  return (login_absence_at?(start_time,end_time) ? true : false)
 end
end

按照最初写测试代码的思路,本方法中存在三种情况,即需要三个用例,而且还调用了其他两个方法,需要对他们进行stub,于是就有了下面的测试代码。记得当时完成后还很兴奋,心中还想:这么写测试代码真有趣。

before(:each) do
 @user = User.new
end

describe "method <absence_at(start_time,end_time)>" do
 s = Time.now
 e = s + 30.minutes
 # example one
 it "should be false when user have interaction or review" do
  @user.stub!(:have_connection_or_review?).with(s,e).and_return(true)
  @user.absence_at(s,e).should be_false
 end

 # example two
 it "should be true when user has no interaction and he no waiting at platform" do
  @user.stub!(:have_connection_or_review?).with(s,e).and_return(false)
  @user.stub!(:login_absence_at?).with(s,e).and_return(true)
  @user.absence_at(s,e).should be_true
 end

 # example three
 it "should be false when user has no interaction and he waiting at platform" do
  @user.stub!(:have_connection_or_review?).with(s,e).and_return(false)
  @user.stub!(:login_absence_at?).with(s,e).and_return(false)
  @user.absence_at(s,e).should be_false
 end
end

上面的测试代码,是典型把代码的实现细节带到了测试代码中,完全是本末倒置的。当然这个测试代码运行的时候,结果都是正确的。那是因为用stub来假定所有的子方法都是对的,但是如果这个子方法have_connection_or_review?发生变化,它不返回boolean值,那么将会发生什么呢?这个测试代码依然正确,可怕吧!这都没有起到测试代码的作用。

另外,如果是这样,我们不仅要修改have_connection_or_review?的测试代码,而且还要修改absence_at的测试代码。这不是在增大代码维护量吗?

相比而言,不用stub的测试代码,不用修改,如果Factory的数据没有发生变化,那么测试代码的结果将是错误的,因为have_connection_or_review?没有通过测试,导致absence_at方法无法正常运行。

其实stub主要是mock一些本方法或者本应用中无法得到的对象,比如在tech_finish?方法中,调用了一个file_service来获得Record对象的所有文件,在本方法测试代码运行过程中,无法得到这个service,这时stub就起作用了:

class A < ActiveRecord::Base
 has_many :records
 def tech_finish?
  self.records.each do |v_a|
   return true if v_a.files.size == 5
  end
  return false
 end
end

class Record < ActiveRecord::Base
 belongs_to :a
 has_files # here is a service in gem
end

所对应的测试代码如下:

describe "tech_finish?" do
 it "should return true when A's records have five files" do
  record = Factory(:record)
  app = Factory(:a,:records=>[record])
  record.stub!(:files).and_return([1,2,3,4,5])
  app.tech_finish?.should == true
 end

 it "should return false when A's records have less five files" do
  record = Factory(:record)
  app = Factory(:a,:records=>[record])
  record.stub!(:files).and_return([1,2,3,5])
  app.tech_finish?.should == false
 end
end

Factory的使用

有了这个工厂,可以很方便的构造不同的模拟数据来运行测试代码。还是上面的例子,如果要测试absence_at方法,涉及到多个model:

  •     HistoryRecord:User的上课记录
  •     Calendar:User的课程表
  •     Logging:User的日志信息

如果不用factory-girl构造测试数据,我们将不得不在fixture构造这些测试数据。在fixture构造的数据无法指定是那个测试用例使用,但是如果用Factory的话,可以为这个方法专门指定一组测试数据。

Factory.define :user_absence_example,:class => User do |user|
 user.login "test"
 class << user
  def default_history_records
   [Factory.build(:history_record,:started_at=>Time.now),
    Factory.build(:history_record,:started_at=>Time.now)]
  end
  def default_calendars
   [Factory.build(:calendar),
    Factory.build(:calendar)]
   end
   def default_loggings
   [Factory.build(:logging,:started_at=>1.days.ago),
    Factory.build(:logging,:started_at=>1.days.ago)]
   end
  end
  user.history_records {default_history_records}
  user.calendars {default_calendars}
  user.loggings {default_loggings}
end

这个测试数据的构造工厂,可以放在factories.rb文件中,方便其他测试用例使用,也可以直接放到测试文件的before中,仅供本测试文件使用。通过factory的构造,不仅可以为多个测试用例共享同一组测试数据,而且测试代码也简洁明了。

before :each do
 @user = Factory.create(:user_absence_example)
end

Readonly的测试

在笔者的系统中,大量使用了acts_as_readonly,从另外一个数据库来读取数据。由于这些model并不在本系统中,所以当用Factory构造测试数据的时候,总会有问题。虽然也可以使用mock来达到这个目的,但是由于mock的局限性,还是无法灵活的满足构造测试数据的需要。为此,扩展了一些代码,使得这些model依然可以测试。核心思想则是,根据配置文件的设置,将对应的readonly的表创建在测试数据库,这个操作在运行测试之前执行,这样就达到与其他model一样的效果。site_config配置文件中,关于readonly的配置格式如下:

readonly_for_test:
 logings:
  datetime: created_at
  string: status
  integer: trainer_id

Gem的测试

Gem在Rails中被广泛使用,而且是最基础的东西,因此它的准确无误就显得更加重要。在不断实践的基础上,笔者所在的团队总结出一种用spec测试gem的方法。假设我们要测试的gem是platform_base,步骤如下:

1. 在gem的根目录下创建一个目录spec(路径为platform_base/spec)。

2. 在gem的根目录下创建文件Rakefile(路径为platform_base/Rakefile),内容如下:

require 'rubygems'
require 'rake'

require 'spec/rake/spectask'

Spec::Rake::SpecTask.new('spec') do |t|
 t.spec_opts = ['--options', "spec/spec.opts"]
 t.spec_files = FileList['spec/**/*_spec.rb']
end

3. 文件在spec目录下创建spec.opts(路径为platform_base/spec/spec.opts),内容如下:

复制代码 代码如下:

--colour
--format progress
--loadby mtime
--reverse

4. 在spec目录下,创建一个Rails app,名为test_app。这个新应用需要有spec目录和spec_helper.rb文件。

5. 为了保持简化,把这个新app(test_app)整理一下,删除vendor和public目录,最终的结构如下:

复制代码 代码如下:

test_app
   |- app
   |- config
   |   |- environments
   |   |- initializers
   |   |- app_config.yml
   |   |- boot.rb
   |   |- database.yml
   |   |- environment.rb
   |   \- routes.rb
   |- db
   |   \- test.sqlite3
   |- log
   \- spec
       \- spec_helper.rb

6. 在config/environment.rb配置文件中,增加如下代码:

Rails::Initializer.run do |config|
 config.gem 'rails_platform_base'
end

7. 在platform_base/spec/目录下增加helpers_spec.rb文件,内容如下:

require File.join(File.dirname(__FILE__), 'test_app/spec/spec_helper')

describe "helpers" do
 describe "url_of" do
  before do
   Rails.stub!(:env).and_return("development")
   @controller = ActionController::Base.new
  end

  it "should get url from app's configration" do
   @controller.url_of(:article, :comments, :article_id => 1).should == "http://www.idapted.com/article/articles/1/comments"
   @controller.url_of(:article, :comments, :article_id => 1, :params=>{:category=>"good"}).should == "http://www.idapted.com/article/articles/1/comments?category=good"
  end
 end
end

至此,准备工作已经就绪,可以在platform_base目录下,运行rake spec来进行测试,当然现在什么都不会发生,因为还没有测试代码呢。本方法中,最关键的就是下面的require语句,不仅加载了Rails environment,而且把gem在test_app中使用并测试。

require File.join(File.dirname(__FILE__), 'test_app/spec/spec_helper')

Controller的测试

对于controller的测试,一般来说比较简单,基本是三段式:初始化参数、请求方法、返回render或者redirect_to。如下例中,对某个controller的index方法的测试:

describe "index action" do
 it "should render report page with the current month report" do
  controller.stub!(:current_user).and_return(@user)
  get :index,{:flag => “test”}
  response.should render_template("index")
 end
end

有些controller会设置session或者flash,这时的测试代码就一定要检查这个值设置的是否正确,而且还需要增加测试用例来覆盖不同的值,这样才能对方法进行全面的测试。如下例:

describe "create action" do
 it "should donot create new user with wrong params" do
  post :create
  response.should redirect_to(users_path)
  flash[:notice].should == "Create Fail!"
 end

 it "should create a new user with right params" do
  post :create, {:email => "abc@eleutian.com"}
  response.should redirect_to(users_path)
  flash[:notice].should == "Create Successful!"
 end
end

同时,也需要对controller的assigns进行测试,以保证返回正确的数据。如下例:

before(:each) do
 @course = Factory(:course)
end 

describe "show action" do
 it "should render show page when flag != assess and success" do
  get :show, :id => @course.id, :flag =>"test"
  response.should render_template("show")
  assigns[:test_paper].should == @course
  assigns[:flag].should == "test"
 end

 it "should render show page when flag == assess and success" do
  get :show, :id => @course.id, :flag =>"assess"
  response.should render_template("show")
  assigns[:test_paper].should == @course
  assigns[:flag].should == "assess"
 end
end

View的测试

View的测试代码写的比较少,基本上是把核心的view部分集成到controller中来测试。主要用integrate_views方法。如下例:

describe AccountsController do
 integrate_views
 describe "index action" do
  it "should render index.rhtml" do
   get :index
   response.should render_template("index")
   response.should have_tag("a[href=?]",new_account_path)
   response.should have_tag("a[href=?]",new_session_path)
  end
 end
end

总结展望

在写测试代码的时候,并不一定要事无巨细,有些比较简单的方法以及Rails的内部的方法,如named_scope,就完全没有必要测试。本文中,只介绍了用rspec写单元测试的代码,对于集成测试没有涉及,这也是今后努力的一个方向。

另外,用cumumber + rspec + webrat的BDD开发模式也是相当不错的。尤其是cumumber对需求的描述,完全可以用它来做需求分析。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索ruby
ruby on rails、ruby on rails 教程、ruby rails、ruby on rails 安装、ruby on rails 指南,以便于您获取更多的相关知识。

时间: 2024-10-26 00:07:17

对Ruby on Rails进行高效的单元测试的教程_ruby专题的相关文章

Windows下Ruby on Rails开发环境安装配置图文教程_ruby专题

本文详细介绍如何在Windows配置Ruby on Rails 开发环境,希望对ROR初学者能有帮助. 一.下载并安装Ruby Windows下安装Ruby最好选择 RubyInstaller(一键安装包). 下载地址: http://rubyforge.org/frs/?group_id=167 . 我们这里下载目前较新的rubyinstaller-1.9.3-p0.exe 一键安装包.这个安装包除了包含ruby本身,还有许多有用的扩展(比如gems)和 帮助文档. 双击安装,安装过程出现如下

Ruby与Ruby on Rails框架环境搭建的简明教程_ruby专题

安装Ruby与升级RubyGems提示:在Ubuntu环境下安装过程中,如果提示权限问题,可以使用sudo make和sudo make install. 1.Ruby安装 wget http://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.3-p125.tar.gz \ && tar -xzvf ruby-1.9.3-p125.tar.gz \ && cd ruby-1.9.3-p125 \ && ./configure

Ruby on Rails迁移时的一些注意事项_ruby专题

    把 schema.rb 保存在版本管控之下.     使用 rake db:scheme:load 取代 rake db:migrate 来初始化空的数据库.     使用 rake db:test:prepare 来更新测试数据库的 schema.     避免在表里设置缺省数据.使用模型层来取代. def amount self[:amount] or 0 end     然而 self[:attr_name] 的使用被视为相当常见的,你也可以考虑使用更罗嗦的(争议地可读性更高的)

Ruby on Rails中的ActiveResource使用详解_ruby专题

当 HTTP 响应是一个与存在的格式不同的格式时(XML 和 JSON),需要某些额外的格式解析,创一个你惯用的格式,并在类别中使用它.惯用的格式应当实作下列方法:extension, mime_type,     encode 以及 decode.      module ActiveResource module Formats module Extend module CSVFormat extend self def extension 'csv' end def mime_type '

对优化Ruby on Rails性能的一些办法的探究_ruby专题

1.导致你的 Rails 应用变慢无非以下两个原因: 在不应该将 Ruby and Rails 作为首选的地方使用 Ruby and Rails.(用 Ruby and Rails 做了不擅长做的工作) 过度的消耗内存导致需要利用大量的时间进行垃圾回收. Rails 是个令人愉快的框架,而且 Ruby 也是一个简洁而优雅的语言.但是如果它被滥用,那会相当的影响性能.有很多工作并不适合用 Ruby and Rails,你最好使用其它的工具,比如,数据库在大数据处理上优势明显,R 语言特别适合做统计

Ubuntu上配置Ruby on Rails框架及RubyMine IDE开发环境_ruby专题

准备阶段的碎碎念 在virtualbox安装过程中由于这样那样的原因,产生许多坑. 坑1.关于终端,一定要使用启动器打开 安装完虚拟机,进入系统,我们便会遇到第一个坑,终端软件,建议大家都使用启动器打开终端, 千万不要使用文件管理器,进入文件夹,再右键"在终端中打开",这个时候,环境变量加载不上,尤其在非桌面文件夹进入时.终端加载不上环境变量,大多的软件都没法在终端中执行. 打开终端时,一定要通过启动器,所谓启动器,就是在bantu中放在桌面左侧的侧边栏,很像windows中的快捷方式

在Ruby on Rails上使用Redis Store的方法_ruby专题

Redis Store 是一个专为Ruby应用程序服务的工具包,原生就支持分片,主从复制,编组以及超时和命名空间.此外,在Ruby on Rails上使用它也是非常的简单. 如何使用:对于在Rails上使用Redis Store,首先我们需要在Gemfile文件中添加入口   gem 'redis-rails' gem 'redis-rack-cache' # optional 然后我们就会有如下选择: ## Cache Store # config/environments/productio

简单介绍Ruby on Rails对PostgreSQL数组类型的支持_ruby专题

 我非常高兴在宣布Rails 4.0 现在支持 PostgreSQL 数组类型. 你可以方便的在migration通过 :array => true里创建数组类型的字段. 创建数组类型的字段的时候还可以添加其它的选项(length,default,等等)   create_table :table_with_arrays do |t| t.integer :int_array, :array => true # integer[] t.integer :int_array, :array =&

Ruby On Rails中如何避免N+1问题_ruby专题

N+1问题 N+1问题是数据库访问中最常见的一个性能问题,首先介绍一下什么是N+1问题: 举个例子,我们数据库中有两张表,一个是Customers,一个是Orders.Orders中含有一个外键customer_id,指向了Customers的主键id. 想要得到所有Customer以及其分别对应的Order,一种写法是 SELECT * FROM Customers; 对于每一个Customer: SELECT * FROM Orders WHERE Orders.customer_id =