While writing functional tests for a controller, I came across a scenario where I have a before_filter requesting some information from the database that one of my tests requires. I'm using Factory_girl to generate test data but I want to avoid hitting the database when its not explicitly needed. I'd also like to avoid testing my before_filter method here (I plan to test it in a separate test). As I understand, mocking/stubbing is the way to accomplish this.
My question is, what is the best way to mock/stub this method in this scenario.
My before filter method looks for a site in the db based on a subdomain found in the URL and sets an instance variable to be used in the controller:
#application_controller.rb
def load_site_from_subdomain
@site = Site.first(:conditions => { :subdomain => request.subdomain })
end
My controller that uses this method as a before_filter:
# pages_controller.rb
before_filter :load_site_from_subdomain
def sho开发者_如何学Gow
@page = @site.pages.find_by_id_or_slug(params[:id]).first
respond_to do |format|
format.html { render_themed_template }
format.xml { render :xml => @page }
end
end
As you can see, it relies on the @site
variable to be set (by the before_filter). During testing however, I'd like to have the test assume that @site
has been set, and that it has at least 1 associated page (found by @site.pages
). I'd like to then test my load_site_from_subdomain
method later.
Here is what I have in my test (using Shoulda & Mocha):
context "a GET request to the #show action" do
setup do
@page = Factory(:page)
@site = Factory.build(:site)
# stub out the @page.site method so it doesn't go
# looking in the db for the site record, this is
# used in this test to add a subdomain to the URL
# when requesting the page
@page.stubs(:site).returns(@site)
# this is where I think I should stub the load_site_from_subdomain
# method, so the @site variable will still be set
# in the controller. I'm just not sure how to do that.
@controller.stubs(:load_site_from_subdomain).returns(@site)
@request.host = "#{ @page.site.subdomain }.example.com"
get :show, :id => @page.id
end
should assign_to(:site)
should assign_to(:page)
should respond_with(:success)
end
This leaves me with an error in my test results telling me that @site
is nil.
I feel like I'm going about this the wrong way. I know it would be easy to simply just Factory.create the site so it exists in the db, but as I said earlier, I'd like to reduce the db usage to help keep my tests speedy.
Try stubbing out 'Site.first' since it the the setting of the @site var that you need to stub and not the returned var from the before_filter.
The reason why your @site
is nil
because your load_site_from_subdomain
does the value assignment for @site
-- it does not return any value hence your stubbing for load_site_from_subdomain
simply doesn't assign the value to @site
. There are two work-arounds for this:
First way:
Change load_site_from_subdomain
to just do a return value:
def load_site_from_subdomain
Site.first(:conditions => { :subdomain => request.subdomain })
end
and then remove the before_filter :load_site_from_subdomain
and change your show
to:
def show
@site = load_site_from_subdomain
@page = @site.pages.find_by_id_or_slug(params[:id]).first
respond_to do |format|
format.html { render_themed_template }
format.xml { render :xml => @page }
end
end
And then do the stubbing in the test:
@controller.stubs(:load_site_from_subdomain).returns(@site)
that ensure our @site
is stubbed indirectly via load_site_from_subdomain
Second way
To stub the Site.first
, I do not really like this approach as in functional test, we do not really care about how the model is retrieved but the behaviour of the respond
. Anyway, if you feel like going this path, you could stub it out in your test:
Site.stubs(:first).returns(@site)
精彩评论