Tutorial on efficient unit tests on Ruby on Rails, rubyrails

Source: Internet
Author: User

Tutorial on efficient unit tests on Ruby on Rails, rubyrails


In the system developed by the author, a large amount of data needs to be analyzed, which not only requires accurate data analysis, but also requires a certain speed. Before writing the test code, I used several major methods to achieve this requirement. As you can imagine, the code is complex, difficult to maintain, and difficult to expand. Taking the opportunity of business adjustment, I decided to start from the test code, and gradually realized the benefits of testing code as I learned and applied it continuously.

  • Change thinking: the process from requirement to code can be transformed and gradually refined;
  • Code simplification: attempts to make every method small and focus only on one thing;
  • Optimization code: when the test code cannot be written, or the code needs to be written for a long time, it indicates that the code is faulty and can be broken down. Further optimization is required;
  • Easy expansion: if the test code fails when the new business is extended or the old business is modified, the expansion and modification are unsuccessful;
  • Half-time: It seems that writing test code is time-consuming. In actual testing, deployment, and subsequent expansion, the test code will save more time.

Environment Construction

The test environment used by the author is a popular and common framework: RSpec + Factory Girl, and autotest automatic tools. RSpec is a descriptive language. It is very easy to get started and easy to understand through feasible examples. Factory Girl can help construct test data, saving you from the trouble of writing fixture. Autotest can automatically run the test code and check the results of the test code at any time. It also supports many plug-ins to make the test results display brilliantly.
Step 1 install rspec and rspec-rails

Execute the following command in the command line:

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

After the installation is complete, go to the directory where the rails application is located and run the following script to generate the spec test framework:

$ 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

Step 2 install factory-girl


Execute the following command in the command line:

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

After the installation is complete, go to the directory where the rails application is located and run the following script to generate the spec test framework:

$ 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

Step 2 install factory-girl

Execute the following command in the command line:

$ sudo gem install factory-girl

In config/environment/test. rb, add the factory-girl gem:

config.gem "factory_girl"

Add a factories. rb file under the spec/directory for all pre-defined model factories.
Step 3 install autotest

Execute the following command in the command line:

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

Then set the integration with RSpec. In the rails application directory, run the following command to display the running results of the test case.

RSPEC = true autotest or autospec

Add a. autotest plug-in your home directory to set all Rails Applications. Of course, you can also add this file to the root directory of each application. This file will overwrite the file settings in the home directory. There are many ins for autotest. The following plugin is used:

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

Set the. autotest file and add the following code to. 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

Test experience

After necessary libraries are installed, you can write test code. In this example, all applications are developed on Rails 2.3.4, and RSpec adopts version 1.3.0. To better illustrate the problem, we assume that we need to determine whether a user is late within a period of time. When writing test code, we follow the principle that we only care about input and output. The specific implementation is not within the scope of the test code, but behavior-driven development. According to this requirement, we will design the method absence_at (start_time, end_time). There are two input values start_time and end_time, and one output value. The type is boolean. The test code is as follows:

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

The test code has been completed. As for the absence_at method, we do not care about its implementation, as long as the result of this method can make the test code running result correct. On the basis of the test code, you can boldly complete the code and continuously modify the code based on the test code results until all test cases pass.
Use of Stub

Write the test code, preferably starting with the model. The model method is easy to use because it fits well with the input and output principles. Initially, you will find that mock and stub are very useful. Any object can be mock, and some methods of stub can be used on the basis of it to save the trouble of constructing data, at one time, I felt that the test code was so beautiful that I found myself in the misunderstanding of stub step by step. Let's refer to the above example. Our code implementation is as follows:

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

According to the idea of writing test code at the beginning, there are three situations in this method: Three Use Cases are required, and two other methods are called. stub needs to be performed on them, so we have the following test code. I remember that I was very excited at the time and thought: It was really interesting to write the test code.

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

The above test code is typically used to bring the implementation details of the Code to the test code, which is totally put before the horse. Of course, the results are correct when the test code is run. That's because stub is used to assume that all sub-methods are correct, but if this sub-method have_connection_or_review? What will happen if it does not return a boolean value? This test code is still correct, terrible! This does not play a role in testing code.

In addition, if so, we should not only modify have_connection_or_review? And modify the test code of absence_at. Isn't this increasing the amount of code maintenance?

In comparison, you do not need to use the stub test code or modify it. If the Factory data does not change, the test code result will be incorrect because have_connection_or_review? If the test fails, the absence_at method cannot run normally.

In fact, stub mainly refers to mock objects that cannot be obtained in this method or in this application, such as in tech_finish? In the method, call a file_service to obtain all the files of the Record object. During the code running of this method, the service cannot be obtained. Then stub takes effect:

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

The test code is as follows:

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

Use of Factory

With this factory, we can easily construct different simulation data to run the test code. In the above example, if you want to test the absence_at method, multiple models are involved:

  • HistoryRecord: Class record of the User
  • Calendar: User's curriculum
  • Logging: User log information

If you do not use factory-girl to construct test data, we will have to construct the test data in fixture. The data constructed in fixture cannot be specified as the test case, but if the Factory is used, a set of test data can be specified for this method.

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

The construction factory of the test data can be stored in the factories. rb file to facilitate use of other test cases. It can also be directly stored in the before file of the test file for use only in this test file. Through the construction of factory, not only can the same group of test data be shared for multiple test cases, but also the test code is concise and clear.

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

Readonly Test

In my system, acts_as_readonly is widely used to read data from another database. Because these models are not in the system, there will always be problems when constructing test data with Factory. Although mock can also be used for this purpose, due to the limitations of mock, it still cannot meet the needs of constructing test data flexibly. To this end, some code is extended so that these models can still be tested. The core idea is to create the corresponding readonly table in the test database according to the configuration file settings. This operation is executed before the test is run, so as to achieve the same effect as other models. In the site_config configuration file, the readonly configuration format is as follows:

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

Gem Testing

Gem is widely used in Rails and is the most basic thing, so its accuracy becomes more important. On the basis of continuous practice, my team summarized a method to test the gem using spec. Assume that the gem we want to test is platform_base. The steps are as follows:

1. Create a directory spec (Path: platform_base/spec) under the root directory of gem ).

2. Create the file Rakefile (Path: platform_base/Rakefile) in the root directory of gem. The content is as follows:

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. Create the spec. opts file in the spec directory (Path: platform_base/spec. opts). The content is as follows:

Copy codeThe Code is as follows: -- color
-- Format progress
-- Loadby mtime
-- Reverse

4. Create a Rails app named test_app under the spec directory. This new application requires the spec directory and the spec_helper.rb file.

5. To simplify the process, sort out the new app (test_app) and delete the vendor and public directories. The final structure is as follows:

Copy codeThe Code is as follows: 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. Add the following code in the config/environment. rb configuration file:

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

7. Add the helpers_spec.rb file in the platform_base/spec/directory. The content is as follows:

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

Now, the preparation is ready. You can run rake spec in the platform_base directory for testing. Of course, nothing will happen now, because no testing code is available. In this method, the most important thing is the following require statement, which not only loads Rails environment, but also uses and tests gem in test_app.

Require File. join (File. dirname (_ FILE _), 'test _ app/spec/spec_helper ')

Controller Testing

The controller Test is generally relatively simple and involves three steps: initialization parameters, request methods, returning render or redirect_to. In the following example, test the index method of a controller:

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

Some controllers will set session or flash. In this case, the test code must check whether the value is set correctly, and add test cases to overwrite different values, in this way, the method can be fully tested. For example:

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

At the same time, you also need to test the assigns of the controller to ensure that the correct data is returned. For example:

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 Test

The test code of View is rarely written. Basically, the core view part is integrated into the controller for testing. We mainly use the integrate_views method. For example:

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

Summary and prospects

When writing test code, it is not necessary to do anything. Some simple methods and internal Rails methods, such as named_scope, are completely unnecessary. This article only introduces the code for writing unit tests using rspec, which is not involved in integration tests. This is also a direction for future efforts.

In addition, the Development Mode of BDD with cumumber + rspec + webrat is also quite good. In particular, cumumber's requirement description can be used for requirement analysis.


Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.