Capybara, Cucumber and How the Cookie Crumbles
When I write a new Rails application, it often needs some sort of user authentication. I like to test authentication in Cucumber and a typical happy-path scenario might look like:
Scenario: Happy path authentication
Given the following user exists:
| Email | Password |
| steve@collectiveidea.com | secret |
When I go to the homepage
Then I should see "Sign In"
But I should not see "Sign Out"
When I follow "Sign In"
And I fill in the following:
| Email | steve@collectiveidea.com |
| Password | secret |
And I press "Continue"
Then I should see "Sign Out"
But I should not see "Sign In"
You’ll notice that the Given
step above requires no action from the user. Givens simply set the stage for the rest of the scenario.
This will be important later.
Fast Forward…
So now that I have authentication working in my application, I’d like to use it elsewhere in my Cucumber suite:
Scenario: User email updation
Given a user exists with an email of "steve@collectiveidea.com"
And I am signed in as "steve@collectiveidea.com"
When I go to the edit account page
And I fill in "Email" with "steve@gemnasium.com"
And I press "Save"
Then I should be on the account page
And I should see "steve@gemnasium.com"
But I should not see "steve@collectiveidea.com"
But how do I write the second step to sign in my user?
Your first instinct may be to replicate the steps from our authentication scenario, but this can be a bad idea. After all, this is a Given
. All I want to do is set the stage. I’ll be using this step quite a bit and the overhead of two additional requests can stack up quickly.
What I really want to do is to set the set the signed-in user directly. Here’s the current\_user
helper method in my ApplicationController
:
def current_user
return @current_user if defined?(@current_user)
@current_user = cookies[:token] && User.find_by_token(cookies[:token])
end
And there inlies my answer… Let’s just set the cookie!
When /^I am signed in as "([^"]*)"$/ do |email|
cookies[:token] = User.find_by_email!(email).token
end
And in some cases, this works great. If you use only the Rack::Test driver and avoid permanent or signed cookies, you should be all set. But as soon as you need your authentication step in a JavaScript scenario, it all falls apart.
Putting the Pieces Together
Long story short: each Capybara driver handles its cookies differently. The cookies hash we access in our step is specific to Rack::Test and is actually a Rack::Test::CookieJar
object.
If you want your application cookies to Just Work™ from anywhere in your Cucumber suite, throw the following into features/support/cookies.rb
:
module Capybara
class Session
def cookies
@cookies ||= begin
secret = Rails.application.config.secret_token
cookies = ActionDispatch::Cookies::CookieJar.new(secret)
cookies.stub(:close!)
cookies
end
end
end
end
Before do
request = ActionDispatch::Request.any_instance
request.stub(:cookie_jar).and_return{ page.cookies }
request.stub(:cookies).and_return{ page.cookies }
end
You’ll need a stubbing library. I’m using RSpec.
This allows each of your Capybara sessions to keep its own separate set of cookies. And they’re real cookies, meaning that you can use cookies.permananent
and cookies.signed
just like you do in your controllers. Then, after each scenario, Capybara will clean its sessions, along with your cookies.
Just use page.cookies
and you’re good to go!
When /^I am signed in as "([^"]*)"$/ do |email|
page.cookies[:token] = User.find_by_email!(email).token
end
Comments
Great post Steve– this is really useful. I’m putting it into my base app. Thanks, Joel
Dude, that looks awesome. Look forward to giving this a try. I have been trying to crack this nut for a long time
Alternative approach we discussed yesterday would be to extract current_user logic from ApplicationController into CurrentUserFinder.find(request) method (new module) and then simply stub it in specs to simulate working with a logged in user.
This way you aren’t dependent on the session transport (cookies, server, etc), and it sends a better message (you make CurrentUserFinder module return the user to treat as current).
@Aleksey if you’re using Devise for authentication and want to stub out the the current_user session you can use Warden directly and do it like this: http://schneems.com/post/15948562424/speed-up-capybara-tests-with-devise
Pity this doesn’t work with Mocha (no dynamic return values on stubs). Not that I much like Mocha, but we use it at my job.
Interesting. It doesn’t work for me thou. I am receiving “undefined method ‘any_instance’”. Is there an extra setup step required somewhere?
@robert: You may need to
require "cucumber/rspec/doubles"
somewhere in your Cucumber setup.@steve Thank you for answering so fast!
Excellent! It works now!
Is “Before” a ruby keyword? Or something specific to rails?
When does the following line get run:
Before do
request = ActionDispatch::Request.any_instance
request.stub(:cookie_jar).and_return{ page.cookies }
request.stub(:cookies).and_return{ page.cookies }
end
@Darek: “Before” is a Cucumber thing. In this case, it runs before each Cucumber scenario. More on Cucumber hooks here.
This. I love the idea of eliminating the extra steps involved in the repeated login process.
Worked like a charm in my request specs, however caused a very difficult to trace bug in controller specs. Generally, any requests from the controller specs fail with “stack level too deep”. Removing the cookie stub (cookies.rb) returns everything back to normal.
I’m using capybara (1.1.2), rspec (2.11.0)
I am getting Timeout:Error. Can anybody guide me to resolve this issue?
I played with this solution, but wasn’t able to get it working with capybara/poltegeist tests. I think the stubbed methods didn’t carry over to the other thread or something. And besides, this requires a heavy duty mocking library like mocha or rspec-mocks. minitest’s mocks don’t quite have the features required to make this work. And I didn’t want to pull in an entire mocking framework just for this. So here’s what I did:
class ActionDispatch::Request
class << self
attr_accessor :stubbed_cookies
def stub_cookies(cookies)
self.stubbed_cookies = cookies
class_eval do
alias :orig_cookies :cookies
alias :orig_cookie_jar :cookie_jar
def cookies
ActionDispatch::Request.stubbed_cookies
end
alias :cookie_jar :cookies
end
end
def unstub_cookies
self.stubbed_cookies=nil
class_eval do
alias :cookie_jar :orig_cookie_jar
alias :cookies :orig_cookies
end
end
end
end
So basically, the same solution as in the post, but without using a mocking framework.
page.cookies no longer works? It doesn’t work for me.
After a lot of soul searching and web surfing, I finally opt’ed for a very simple and obvious solution.
Using cookies adds two problems. First you have code in the application specific for testing and second there is the problem that creating cookies in Cucumber is hard when using anything other than rack test. There are various solutions to the cookie problem but all of them are a bit challenging, some introduce mocks, and all of them are what I call ‘tricky’.
My solution is the following. This is using HTTP basic authentication but it could be generalized for most anything.
authenticate_or_request_with_http_basic “My Authentication” do |user_name, password|
if Rails.env.test? && user_name == ‘testuser’
test_authenticate(user_name, password)
else
normal_authentication
end
end
test_authenticate does what ever the normal authenticate does except it bypasses any time consuming parts. In my case, the real authentication is using LDAP which i wanted to avoid.
Yes… it is a bit gross but it is clear, simple, and obvious. And… no other solution I’ve seen is cleaner or clearer.
Note that if the user_name is not ‘test user’, then the normal path is taken so it can be tested.
I hope others find this useful.
One issue with this solution: the cookies don’t seem to clear between tests anymore.
All of my tests assume that the user is not logged in at the start. My second and subsequent tests are failing because the user is still logged in from the previous session.
Is there some way to fix this?
Any chance this gets an article gets an update for Rails 4? The ‘permanent’ and ‘signed’ instance methods have been pulled out into a module called ‘ChainedCookieJars’ so this implementation is out of date. It worked great in Rails 3x though :)
This piece of writing will help the internet
viewers for creating new blog or even a weblog from
start to end.
I used to be recommended this blog via my cousin. I’m not sure whether or not
this publish is written by way of him as nobody else recognise such certain about my problem.
You’re incredible! Thanks!
Needed to add this to Capybara::Session:
```
def clear_cookies
@cookies = nil
end
```
And this after hook:
```
After do
page.clear_cookies
end
```
This prevents the session from persisting from one feature to the next.