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.