I have a simple controller test, containing a.o. the following code:
context "POST :create" do
before (:each) do
post :create, :user_id => @user.id,
:account => { .. some data ... }
end
it { response.status.should == 201 }
it { response.location.should be_present }
end
Now I thought of a very simpl开发者_如何学运维e way to speed up this test, and to use a before(:all)
instead of a before(:each)
. In that case the post would only be done once.
So i wrote:
context "POST :create" do
before (:all) do
post :create, :user_id => @user.id,
:account => { .. some data ... }
end
it { response.status.should == 201 }
it { response.location.should be_present }
end
But then I get the following errors:
RuntimeError:
@routes is nil: make sure you set it in your test's setup method.
Is this by design? Is there a way to circumvent it?
I asked this question on the rspec mailing list, and got the following reply from @dchelimsky himself:
Yes. rspec-rails wraps the rails' testing framework which doesn't have a before(:all) concept in it, so all the data is reset before each example. Even if we wanted to support this in rspec-rails (which I don't) it would require changes to rails first.
So doing controller calls is not possible in a before(:all)
, it can only be used to setup your DB or instance variables.
If you want to go the dirty global variable way and benefit from the speeding increase, you can use this but caution. This messy logic does the job but defeats the purpose of driving with crystal clear readable tests. Refactoring in a helper with yield is more than recommended.
describe PagesController do
describe "GET 'index'" do
before(:each) do
GLOBAL ||= {}
@response = GLOBAL[Time.now.to_f] || begin
get :index
response
end
end
it { @response.should redirect_to(root_path) }
it { @response.status.should == 301 }
it { @response.location.should be_present }
end
end
The refactor you can put into a file of your choice in spec/support goes as follow
RSPEC_GLOBAL = {}
def remember_through_each_test_of_current_scope(variable_name)
self.instance_variable_set("@#{variable_name}", RSPEC_GLOBAL[variable_name] || begin
yield
end)
RSPEC_GLOBAL[variable_name] ||= self.instance_variable_get("@#{variable_name}")
end
Thus, the code in test file becomes :
describe PagesController do
describe "GET 'index'" do
before(:each) do
remember_through_each_test_of_current_scope('memoized_response') do
get :index
response
end
end
it { @memoized_response.should redirect_to(root_path) }
it { @memoized_response.status.should == 301 }
it { @memoized_response.location.should be_present }
end
end
Hope it helps, and once again, use with caution
I'm not sure if this is a good idea, but setting a class variable with ||=
in the before(:each)
block seems to work:
describe PagesController do
describe "GET 'index'" do
before(:each) do
@@response ||= begin
get :index
response
end
end
it { @@response.should redirect_to(root_path) }
it { @@response.status.should == 301 }
it { @@response.location.should be_present }
end
end
Update
Another potentially cleaner approach is to have multiple assertions in a single spec. Adding the :aggregate_failures
tag (or wrapping the assertions in an aggregate_failures {...}
block) will print each failure separately, which provides the granularity of separate tests:
describe PagesController do
describe "GET 'index'" do
it "redirects to homepage", :aggregate_failures do
get :index
expect(response).to redirect_to(root_path)
expect(response.status).to eq(301)
expect(response.location).to be_present
end
end
end
精彩评论