Collective Idea

Collective Idea Logo

Steve Richert

Capybara, Cucumber and How the Cookie Crumbles

By Steve Richert on January 05, 2012 in capybara, cookies, cucumber, and rails

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
By Steve Richert on January 05, 2012 in capybara, cookies, cucumber, and rails

22 Comments

  1. Joel Parker Henderson

    Joel Parker Henderson January 11, 2012

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

  2. Jonathan

    Jonathan January 13, 2012 http://pipewave.com

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

  3. Aleksey Gureiev

    Aleksey Gureiev January 16, 2012 http://blog.noizeramp.com/

    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. Richard Schneeman

    Richard Schneeman January 16, 2012 http://schneems.com

    @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. Marnen Laibow-Koser

    Marnen Laibow-Koser January 18, 2012 http://www.marnen.org

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

    robert February 23, 2012 http://www.ameil.com

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

  7. Steve Richert

    Steve Richert February 23, 2012 http://collectiveidea.com

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

  8. Benjamin Lewis

    Benjamin Lewis February 23, 2012 http://23inhouse.com

    @steve Thank you for answering so fast!

  9. robert

    robert February 23, 2012 http://www.ameil.com

    Excellent! It works now!

  10. Darek

    Darek April 20, 2012

    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. Steve Richert

    Steve Richert April 20, 2012 http://collectiveidea.com

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

  12. Quigg

    Quigg July 25, 2012

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

  13. Konstantin

    Konstantin September 04, 2012

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

    Mani September 27, 2012

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

  15. Russ Egan

    Russ Egan November 27, 2012

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

    pedz March 10, 2013

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

  17. pedz

    pedz March 10, 2013

    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 Birchmeier

    Grant Birchmeier June 06, 2013

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

    Louie August 05, 2013

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

    pills October 18, 2013 http://www.tramadolinfos.com/

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

  21. Tracee

    Tracee February 14, 2014 http://imgur.com/baBl7YW

    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. Anonymous Guy

    Anonymous Guy March 04, 2014

    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.

Post a Comment

Contact Us

Find us on Google Maps
Collective Idea
44 East 8th Street, Suite 410
Holland, Michigan 49423 USA 42.790334-86.105251

Follow us on the Interwebs

We are currently available for medium and long term projects. Please get in touch if we can be of service.