Devise, Rspec, user expectations
I am using Devise for authentication obviously 开发者_StackOverflow社区and what I am trying to do is test that a method is invoked on the user object. So my spec looks like this:
it "should retrieve something for user" do
@user = Factory.create(:user)
sign_in @user
@user.expects(:something)
get :manage
end
the problem I have is the expects fails unless I do:
it "should retrieve something for user" do
@user = Factory.create(:user)
sign_in @user
controller.stubs(:current_user).returns @user
@user.expects(:something)
get :manage
end
faking out the current_user call on the controller seems hokey if sign_in @user is a devise test helper. after digging in the debugger also it appears when not faking out current_user the @user is really being returned so Im not sure why the expectation is not met.
Ideas?
The current user is serialized before storing in the session; likely, for an ActiveRecord User model, it's storing the user's id:
https://github.com/plataformatec/devise/blob/master/lib/devise/test_helpers.rb#L49
This means that when your controller fetches the user again:
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/helpers.rb#L47-49
it's finding the user_id in the session, and re-fetching the User from the database.
This means that the User object returned by #current_user in the controller is a different Ruby object than the @user you pass into #sign_in in your test, so stubs and expectations from one object aren't attached to the other.
I haven't used Devise/Warden extensively, but am pretty sure this is what's going on. You can try printing out #object_id on the two instances to confirm this.
Picky semantics update Feb 2014:
Stubbing #current_user
on your controller is a fine approach here, depending on your definition of the "boundary" of the system under test (SUT) -- i.e. your controller. Assuming the #current_user
method is defined by Devise and mixed into your controller by Devise, you could argue that #current_user
is external to your SUT and therefore fair game for stubbing.
You could alternatively stub the underlying layer that Devise is accessing (ActiveRecord), say by stubbing User.find
to return your @user
object. This means your spec is testing both your action implementation as well as the Devise implementation of #current_user
, so the spec would fail if either of those things changed later.
Let's say Devise uses User.find(args)
, and say you stub that method, and then a later version of Devise changed to use User.where(args).first()
- your code hasn't changed, but the underlying library has, and your spec fails. Thinking very generally about this idea, there are times when you'd probably like that behavior (think about mocking a raw HTTP response with e.g. WebMock instead of stubbing Net::HTTP
methods so you could later swap http libraries), and times you wouldn't (maybe this Devise question counts as one).
精彩评论