开发者

Speeding up rspec controllers test: using before all fails?

开发者 https://www.devze.com 2023-04-11 00:23 出处:网络
I have a simple controller test, containing a.o. the following code: context \"POST :create\" do before (:each) do

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
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号