tutorial on efficient unit testing of Ruby on Rails _ruby topics

Source: Internet
Author: User
Tags stub ruby on rails



In the system developed by the author, there is a large number of data needs analysis, not only the data analysis is accurate, but also has certain requirements for speed. Before writing the test code, the author used several great methods to achieve this requirement. The result is conceivable, the code is complex, maintenance difficult, difficult to expand. The opportunity to adjust the business, the author, decided to start from the test code, and with continuous learning and application, slowly realize the benefits of testing code.


    • Change the train of thought: can do from demand to code process conversion, gradually refinement;
    • Simplify code: Try to make each method small, focusing only on one thing;
    • Optimization code: When the test code is not written, or need to write a long time, the description of the code is problematic, can be decomposed, need further optimization;
    • Easy to extend: When the new business is expanded or the old business is modified, if the test code does not succeed, the extension and modification are unsuccessful;
    • Time-half power times: Seemingly write test code is time-consuming, in fact, in testing, deployment and subsequent extensions, the test code will save more time.


Environment construction



The author adopts the test environment is more popular general framework: RSpec + Factory Girl, and autotest automatic tool. RSpec is a descriptive language, which is easy to understand by describing system behavior in a workable example. Factory Girl can help to construct test data, eliminating the trouble of writing fixture. Autotest can automatically run the test code, at any time to detect the results of the test code, and there are a lot of plug-ins to support, you can make the test results appear very cool.
The first step is to install RSpec and rspec-rails



On the command line, execute the following command:


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


When the installation is complete, enter 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


Second Step installation Factory-girl




On the command line, execute the following command:


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


When the installation is complete, enter 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


Second Step installation Factory-girl



On the command line, execute the following command:


$ sudo gem install Factory-girl


In Config/environment/test.rb, join the Gem of Factory-girl:


Config.gem "Factory_girl"


In the spec/directory, add a factories.rb file for all predefined model factories.
Step three Install Autotest



On the command line, execute the following command:


$ sudo gem install zentest
$ sudo gem install autotest-rails


Then set up the integration with RSpec, and in the Rails application directory, run the following commands to display the test case run results.



Rspec=true Autotest or Autospec



In your home directory, add a. Autotest to set up all the Autotest plug-ins for the rails application. Of course, you can also add this file to the root directory of each application, which will overwrite the file settings in the home directory. Autotest Plug-ins are many, the author used the following plugin:


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


Set up the. autotest file, and in. Autotest, add the following code.


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



Test experience



After you have installed the necessary libraries, you can write your test code. In this case, all applications are developed on Rails 2.3.4 and RSpec are 1.3.0 versions. To illustrate the problem well, we assume that the requirement is to determine whether a user is late for a period of time. Write test code is to follow a principle, only care about input and output, the specific implementation is not within the scope of the test code, is the behavior-driven development. Based on this requirement, we will design method Absence_at (start_time,end_time) with two input values start_time and end_time as well as an output value, the type is Boolean. The corresponding test code is as follows:


Describe "user absence or not during [Start_time,end_time]" does
 Before:each do 
  @user = Factory (: User)
 endit "should return false when user does 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


The test code has been completed. As for the Absence_at method we do not care about its implementation, as long as the results of this method will allow the test code to run the correct results. Based on this test code, you can boldly complete the code and constantly modify the code according to the results of the test code until all the test cases pass.
Use of stub



Write test code, preferably starting from model. Because model method can be very good with the principle of input and output, easy to get started. At first, you will find that the mock and stub is very good, any object can be mock, and on its basis can stub some methods, save the construction of data trouble, once let the author feel that the test code is so beautiful, step-by-step in-depth, only to find themselves into a stub error. or 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



According to the original writing test code, there are three cases in this method that require three use cases, and also call the other two methods, which need to be stub, and then have the following test code. I remember being excited when I finished, and thinking, "It's 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 typical of the code implementation details into the test code, completely put the cart before the horse. Of course, when the test code runs, the results are correct. That's because the stub assumes that all the sub methods are true, but what happens if this have_connection_or_review changes and does not return a Boolean value? This test code is still correct, terrible! None of this has the effect of testing the code.



In addition, if so, we will not only modify the Have_connection_or_review test code, but also modify the Absence_at test code. Isn't this increasing the amount of code maintenance?



In contrast, without stub test code, without modification, if the factory data does not change, then the result of the test code will be wrong, because Have_connection_or_review? Failed to pass the test, resulting in absence_ The At method does not function correctly.



In fact, the stub is mainly to mock some of this method or the application can not get the object, such as in the Tech_finish method, called a file_service to get all the files of the Record object, in this method test code run process, Unable to get the service, the stub worked:


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 class record

< activerecord::base< C10/>belongs_to:a
 Has_files # Here's a service in the gem end



The corresponding test code is as follows:


Describe "Tech_finish" "Do
 it" should return true when A ' s records have five files ' did 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" does record
  = Factory (: Record)
  app = Factory (: A,:records=>[record])
  record.stub! (: Files). And_return ([1,2,3,5])   
  App.tech_ Finish?. should = = False End



The use of factory



With this factory, it is convenient to construct different simulated data to run the test code. Or the above example, if you want to test the Absence_at method, involves multiple model:


    • Historyrecord:user's class record.
    • Calendar:user's Timetable
    • Log information for Logging:user


If the test data is not factory-girl constructed, we will have to construct these test data in fixture. The data constructed in fixture cannot be specified as the test case, but you can specifically specify a set of test data for this method if you are using 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


The construction of this test data factory, can be placed in the Factories.rb file, to facilitate the use of other test cases, can also be placed directly in the before of the test file, only for this test file use. Through the construction of factory, not only can you share the same set of test data for multiple test cases, but also the test code is concise and clear.


Before:each do
 @user = factory.create (: user_absence_example)
End


ReadOnly's Test



In the author's system, a lot of use of acts_as_readonly, from another database to read data. Since these model is not in the system, so when using factory to construct test data, there will always be problems. Although you can use mocks to do this, you cannot flexibly meet the need to construct test data because of the limitations of mocks. To this end, some code has been extended so that the model can still be tested. The core idea is that, depending on the configuration file settings, the corresponding readonly table is created in the test database, which executes before running the test, so that it achieves the same effect as the other model. In the Site_config configuration file, the configuration format for ReadOnly is as follows:


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


The test of the gem



Gem is widely used in rails and is the most basic thing, so it is more important to be accurate. On the basis of continuous practice, the author's team summed up a way to test the gem with the spec. Suppose that the gem we are testing is platform_base, and the steps are as follows:



1. Create a catalog spec in the root of the gem (path is Platform_base/spec).



2. Create the file Rakefile in the root of the gem (path is Platform_base/rakefile) 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. File in the Spec directory to create spec.opts (path is platform_base/spec/spec.opts), the contents are as follows:





Copy Code code as follows:
--colour
--format Progress
--loadby Mtime
--reverse





4. In the spec directory, create a rails app named Test_app. This new application requires a spec catalog and a spec_helper.rb file.



5. To keep it simple, tidy up this new app (Test_app), delete the vendor and public directories, and the final structure is as follows:





Copy Code code 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. In the CONFIG/ENVIRONMENT.RB configuration file, add the following code:


Rails::initializer.run do |config|
 Config.gem ' rails_platform_base ' End



7. Add the HELPERS_SPEC.RB document under the platform_base/spec/directory, which reads as follows:



Require File.join (File.dirname (__file__), ' Test_app/spec/spec_helper ')


Describe "helpers" do
 describe "url_of" does before do rails.stub!
   (: env). And_return ("development")
   @ Controller = actioncontroller::base.new
  end

  It "should get URL from app ' 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,:p arams=>{:category=> "good"}). Should = "http:// Www.idapted.com/article/articles/1/comments?category=good "End End"



At this point, the preparation is ready, you can run the rake spec to test in the Platform_base directory, of course, nothing will happen now, because there is no test code yet. The key to this approach is the following require statement, which not only loads rails environment, but also uses and tests gems in Test_app.



Require File.join (File.dirname (__file__), ' Test_app/spec/spec_helper ')



Controller's Test



For controller testing, generally simpler, basically three-stage: initialization parameters, request method, return render or redirect_to. In the following example, a test of the index method for a controller:


Describe "index action" do it "should render the" "The" "Month" and "" "The" "" Do
  controller.stub! (: Curre Nt_user). And_return (@user)
  get:index,{:flag => "Test"}
  response.should render_template ("index")
 End End



Some controller will set session or Flash, then the test code must check whether this value is set correctly, but also need to increase the test case to overwrite different values, so that the method can be fully tested. The following example:


Describe ' Create action ' do
 it ' should donot create new user with wrong params ' do
  post:create
  Response.sho Uld 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 '}
  resp Onse.should redirect_to (users_path)
  flash[:notice].should = = "Create successful!"
 End End



At the same time, it is necessary to test the controller assigns to ensure that the correct data is returned. The following example:


Before (: each) does
 @course = Factory (: course)
end 

describe ' show action ' do
 it ' should render show page whe N Flag!= Assess and Success "do 
  get:show,: ID => @course. ID,: Flag =>" Test "
  response.should Render_templat E ("show")
  assigns[:test_paper].should = = @course
  assigns[:flag].should = = "Test end
 It"

 should Render Show Page flag = = Assess and success do
  get:show: ID => @course. ID,: Flag => "Assess"
  respon Se.should render_template ("show")
  assigns[:test_paper].should = = @course
  Assigns[:flag].should = = "Assess "End End  



View's Test



The view's test code is less written and basically integrates the core view part into the controller to test. Mainly used Integrate_views method. The following 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



Summary Outlook



When writing a test code, there is no need to be exhaustive, there are some simpler methods and the internal methods of rails, such as named_scope, are completely unnecessary to test. In this article, only the code that writes unit tests with RSpec is covered, not for integration testing, which is also a direction for future efforts.



In addition, the BDD development model with Cumumber + RSpec + Webrat is also quite good. In particular, cumumber to the requirements of the description, it can be used to do demand 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.