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
steve@collectiveidea.com

Comments:


Post a Comment

(optional)
(optional — will be included as a link.)
  1. Great post Steve– this is really useful. I’m putting it into my base app. Thanks, Joel

    joel@joelparkerhenderson.com
    Joel Parker Henderson
    January 12, 2012 at 1:20 AM
  2. Dude, that looks awesome. Look forward to giving this a try. I have been trying to crack this nut for a long time

    January 13, 2012 at 14:57 PM
  3. 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).

    January 16, 2012 at 11:44 AM
  4. @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

    January 16, 2012 at 16:22 PM
  5. 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.

    January 18, 2012 at 19:23 PM
  6. Interesting. It doesn’t work for me thou. I am receiving “undefined method ‘any_instance’”. Is there an extra setup step required somewhere?

    February 23, 2012 at 23:30 PM
  7. @robert: You may need to require "cucumber/rspec/doubles" somewhere in your Cucumber setup.

    February 23, 2012 at 23:43 PM
  8. @steve Thank you for answering so fast!

    February 24, 2012 at 0:10 AM
  9. Excellent! It works now!

    February 23, 2012 at 23:56 PM
  10. 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

    pdarek200-man@yahoo.com
    Darek
    April 20, 2012 at 14:03 PM
  11. @Darek: “Before” is a Cucumber thing. In this case, it runs before each Cucumber scenario. More on Cucumber hooks here.

    April 20, 2012 at 14:08 PM
  12. This.  I love the idea of eliminating the extra steps involved in the repeated login process. 

    jonathan.quigg@gmail.com
    Quigg
    July 26, 2012 at 2:58 AM
  13. 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)

    konstantin.k.ed@gmail.com
    Konstantin
    September 04, 2012 at 18:13 PM
  14. I am getting Timeout:Error. Can anybody guide me to resolve this issue?

    nvn.manikandaprabhu@gmail,com
    Mani
    September 27, 2012 at 7:08 AM
  15. 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.

    russellegan@gmail.com
    Russ Egan
    November 27, 2012 at 19:18 PM
  16. page.cookies no longer works? It doesn’t work for me.

    pedzsan@gmail.com
    pedz
    March 10, 2013 at 6:31 AM
  17. 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.

    pedzsan@gmail.com
    pedz
    March 11, 2013 at 0:29 AM
  18. 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?

    grant@grantb.net
    Grant Birchmeier
    June 07, 2013 at 1:25 AM
  19. 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 :)

    louiscmancini@gmail.com
    Louie
    August 05, 2013 at 7:18 AM
  20. This piece of writing will help the internet viewers for creating new blog or even a weblog from start to end.

    October 18, 2013 at 11:58 AM
  21. 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!

    February 14, 2014 at 9:55 AM
  22. 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.

    dontcleareverythingifformvalidationfails@example.com
    Anonymous Guy
    March 04, 2014 at 18:00 PM