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 |
    | [email protected] | 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    | [email protected] |
    | 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 "[email protected]"
  And I am signed in as "[email protected]"
  When I go to the edit account page
  And I fill in "Email" with "[email protected]"
  And I press "Save"
  Then I should be on the account page
  And I should see "[email protected]"
  But I should not see "[email protected]"

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

  1. joel@joelparkerhenderson.com
    Joel Parker Henderson
    January 11, 2012 at 20:20 PM

    Great post Steve– this is really useful. I’m putting it into my base app. Thanks, Joel

  2. January 13, 2012 at 9:57 AM

    Dude, that looks awesome. Look forward to giving this a try. I have been trying to crack this nut for a long time

  3. January 16, 2012 at 6:44 AM

    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).

  4. January 16, 2012 at 11:22 AM

    @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

  5. January 18, 2012 at 14:23 PM

    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.

  6. February 23, 2012 at 18:30 PM

    Interesting. It doesn’t work for me thou. I am receiving “undefined method ‘any_instance’”. Is there an extra setup step required somewhere?

  7. February 23, 2012 at 18:43 PM

    @robert: You may need to require "cucumber/rspec/doubles" somewhere in your Cucumber setup.

  8. February 23, 2012 at 19:10 PM

    @steve Thank you for answering so fast!

  9. February 23, 2012 at 18:56 PM

    Excellent! It works now!

  10. pdarek200-man@yahoo.com
    Darek
    April 20, 2012 at 10:03 AM

    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

  11. April 20, 2012 at 10:08 AM

    @Darek: “Before” is a Cucumber thing. In this case, it runs before each Cucumber scenario. More on Cucumber hooks here.

  12. jonathan.quigg@gmail.com
    Quigg
    July 25, 2012 at 22:58 PM

    This.  I love the idea of eliminating the extra steps involved in the repeated login process. 

  13. konstantin.k.ed@gmail.com
    Konstantin
    September 04, 2012 at 14:13 PM

    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)

  14. nvn.manikandaprabhu@gmail,com
    Mani
    September 27, 2012 at 3:08 AM

    I am getting Timeout:Error. Can anybody guide me to resolve this issue?

  15. russellegan@gmail.com
    Russ Egan
    November 27, 2012 at 14:18 PM

    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.

  16. pedzsan@gmail.com
    pedz
    March 10, 2013 at 1:31 AM

    page.cookies no longer works? It doesn’t work for me.

  17. pedzsan@gmail.com
    pedz
    March 10, 2013 at 20:29 PM

    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.

  18. grant@grantb.net
    Grant Birchmeier
    June 06, 2013 at 21:25 PM

    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?

  19. louiscmancini@gmail.com
    Louie
    August 05, 2013 at 3:18 AM

    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 :)

  20. October 18, 2013 at 7:58 AM

    This piece of writing will help the internet
    viewers for creating new blog or even a weblog from
    start to end.

  21. February 14, 2014 at 4:55 AM

    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!

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

    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.