Session variables with Cucumber Stories
Asked Answered
M

11

16

I am working on some Cucumber stories for a 'sign up' application which has a number of steps.

Rather then writing a Huuuuuuuge story to cover all the steps at once, which would be bad, I'd rather work through each action in the controller like a regular user. My problem here is that I am storing the account ID which is created in the first step as a session variable, so when step 2, step 3 etc are visited the existing registration data is loaded.

I'm aware of being able to access controller.session[..] within RSpec specifications however when I try to do this in Cucumber stories it fails with the following error (and, I've also read somewhere this is an anti-pattern etc...):

Using controller.session[:whatever] or session[:whatever]

You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)

Using session(:whatever)

wrong number of arguments (1 for 0) (ArgumentError)

So, it seems accession the session store isn't really possible. What I'm wondering is if it might be possible to (and I guess which would be best..):

  1. Mock out the session store etc
  2. Have a method within the controller and stub that out (e.g. get_registration which assigns an instance variable...)

I've looked through the RSpec book (well, skimmed) and had a look through WebRat etc, but I haven't really found an answer to my problem...

To clarify a bit more, the signup process is more like a state machine - e.g. the user progresses through four steps before the registration is complete - hence 'logging in' isn't really an option (it breaks the model of how the site works)...

In my spec for the controller I was able to stub out the call to the method which loads the model based on the session var - but I'm not sure if the 'antipattern' line also applies to stubs as well as mocks?

Thanks!

Momus answered 13/8, 2009 at 12:52 Comment(0)
S
19

mocks are bad in cucumber scenarios - they're almost kind of an antipattern.

My suggestion is to write a step that actually logs a user in. I do it this way

Given I am logged in as "[email protected]"

Given /^I am logged in as "(.*)"$/ do |email|
  @user = Factory(:user, :email => email)
  @user.activate!
  visit("/session/new")
  fill_in("email", :with => @user.email)
  fill_in("password", :with => @user.password)
  click_button("Sign In")
end

I realize that the instance variable @user is kind of bad form—but I think in the case of logging in/out, having @user is definitely helpful.

Sometimes I call it @current_user.

Salomon answered 13/8, 2009 at 23:27 Comment(4)
Thanks for the input... I've updated my question with some more details, but what you have suggested doesn't really help as it would involve adding functionality specifically for the tests, which to me seems like more of an 'anti-pattern' - but, would stubbing out controller methods break this rule?Momus
additional functionality? How does a user login to your site? You mention your registration process but not login. Why can't you create a factory for an already registered user and log them in as I've shown you above? Cucumber stories should express exactly how the user interacts with your site. A user can't stub out controller methods. They have forms to fill out and links to click. It's not an acceptance test if it doesn't mimic user behavior.Salomon
I see both sides here. I've got almost exactly the above for the feature that tests authentication works the way I want. For other features, it seems a pain to have to repeat those capybara steps to sign in for every scenario. Cucumber can manipulate the database state for the purpose of a test, why not the session?Axes
As a developer, I know why you want to avoid the 'extra work' of logging in for each step, it feels bad. But that is precisely what you want in a test: each step should be 100% independent from the others. This allows them to be run one-at-a-time, or in a different order with no worry of dependencies between each scenario.Vibraphone
C
25

I'll repeat danpickett in saying mocks should be avoided whenever possible in Cucumber. However if your app does not have a login page, or perhaps performance is a problem, then it may be necessary to simulate login directly.

This is an ugly hack, but it should get the job done.

Given /^I am logged in as "(.*)"$/ do |email|
  @current_user = Factory(:user, :email => email)
  cookies[:stub_user_id] = @current_user.id
end

# in application controller
class ApplicationController < ActionController::Base
  if Rails.env.test?
    prepend_before_filter :stub_current_user
    def stub_current_user
      session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
    end
  end
end
Cancer answered 18/8, 2009 at 16:43 Comment(1)
Just used this to skip the login for an OAuth site. Brilliant. Thanks Ryan.Hackman
S
19

mocks are bad in cucumber scenarios - they're almost kind of an antipattern.

My suggestion is to write a step that actually logs a user in. I do it this way

Given I am logged in as "[email protected]"

Given /^I am logged in as "(.*)"$/ do |email|
  @user = Factory(:user, :email => email)
  @user.activate!
  visit("/session/new")
  fill_in("email", :with => @user.email)
  fill_in("password", :with => @user.password)
  click_button("Sign In")
end

I realize that the instance variable @user is kind of bad form—but I think in the case of logging in/out, having @user is definitely helpful.

Sometimes I call it @current_user.

Salomon answered 13/8, 2009 at 23:27 Comment(4)
Thanks for the input... I've updated my question with some more details, but what you have suggested doesn't really help as it would involve adding functionality specifically for the tests, which to me seems like more of an 'anti-pattern' - but, would stubbing out controller methods break this rule?Momus
additional functionality? How does a user login to your site? You mention your registration process but not login. Why can't you create a factory for an already registered user and log them in as I've shown you above? Cucumber stories should express exactly how the user interacts with your site. A user can't stub out controller methods. They have forms to fill out and links to click. It's not an acceptance test if it doesn't mimic user behavior.Salomon
I see both sides here. I've got almost exactly the above for the feature that tests authentication works the way I want. For other features, it seems a pain to have to repeat those capybara steps to sign in for every scenario. Cucumber can manipulate the database state for the purpose of a test, why not the session?Axes
As a developer, I know why you want to avoid the 'extra work' of logging in for each step, it feels bad. But that is precisely what you want in a test: each step should be 100% independent from the others. This allows them to be run one-at-a-time, or in a different order with no worry of dependencies between each scenario.Vibraphone
B
17

Re. Ryan's solution - you can open up ActionController in you env.rb file and place it there to avoid putting in your production code base (thanks to john @ pivotal labs)

# in features/support/env.rb
class ApplicationController < ActionController::Base
  prepend_before_filter :stub_current_user
  def stub_current_user
    session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
  end
end
Benito answered 15/1, 2010 at 18:59 Comment(1)
I'm in Rails 4, and I'm observing that this "prevents" my existing before_filter functions from happening, as well as preventing my helper_methods from existing.Sleepyhead
F
5

I don't know how much this relates to the original question anymore, but I decided to post anyway in the spirit of discussion...

We have a cucumber test suite that takes > 10 minutes to run so we wanted to do some optimization. In our app the login process triggers a LOT of extra functionality that is irrelevant to majority of the scenarios, so we wanted to skip that by setting the session user id directly.

Ryanb's approach above worked nicely, except that we were unable to log out using that approach. This made our multi-user stories fail.

We ended up creating a "quick login" route that is only enabled in test environment:

# in routes.rb
map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'

Here is the corresponding action that creates the session variable:

# in logins_controller.rb
class LoginsController < ApplicationController
  # This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
  # Please never make this method usable in production/staging environments.
  def quick_login
    raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
    u = User.find_by_login(params[:login])
    if u
      session[:user_id] = u.id
      render :text => "assumed identity of #{u.login}"
    else
      raise "failed to assume identity"
    end
  end
end

For us this ended up being simpler than working with the cookies array. As a bonus, this approach also works with Selenium/Watir.

Downside is that we're including test-related code in our application. Personally I don't think that adding code to make application more testable is a huge sin, even if it does add a bit of clutter. Perhaps the biggest problem is that future test authors need to figure out which type of login they should use. With unlimited hardware performance we obviously wouldn't be doing any of this.

Florencio answered 16/12, 2009 at 14:40 Comment(1)
Interesting idea, and there may be an alternative to peppering your production-ready code with testing code - perhaps when your tests are starting up you could inject the new route and class_eval the application controller to handle the quicklogin code... Just a thought anyways ;) Thanks for your contribution!Momus
M
4

Re: Ryan's solution:

Does not work with Capybara, unless small adaptation done:

rack_test_driver = Capybara.current_session.driver
cookie_jar = rack_test_driver.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
@current_user = Factory(:user)
cookie_jar[:stub_user_id] = @current_user.id

(found here: https://gist.github.com/484787)

Mary answered 18/12, 2010 at 7:56 Comment(2)
Thank for contributing your solution back here again, This helped me a lot, it works like a charm.Varanasi
For me that's just triggering an error: undefined method 'current_session' for #<Capybara::RackTest::Driver:0x20ebe28> Could you be more specific as to where in your application you need to put this code?Windsor
A
3

My understanding is that you get:

You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)

when session[] is accessed before request has been instantiated. In your case, I'd imagine if you put webrats' visit some_existing_path before accessing session[] in your step defenition, the error will go away.

Now, unfortunately, session doesn't seem to persist across steps (at least, I couldn't find the way), so this bit of information doesn't help to answer your question :)

So, I suppose, Ryan's session[:user_id] = cookies[:stub_user_id]... is the way to go. Although, imo, test related code in the application itself doesn't sound right.

Ayrshire answered 7/9, 2009 at 14:23 Comment(0)
R
2

I use a testing-only sign-in solution like Prikka's, but I do it all in Rack instead of creating a new Controller and routes.

# in config/environments/cucumber.rb:

config.middleware.use (Class.new do
  def initialize(app); @app = app; end
  def call(env)
    request = ::Rack::Request.new(env)
    if request.params.has_key?('signed_in_user_id')
      request.session[:current_user_id] = request.params['signed_in_user_id']
    end
    @app.call env
  end
end)

# in features/step_definitions/authentication_steps.rb:
Given /^I am signed in as ([^\"]+)$/ do |name|
  user = User.find_by_username(name) || Factory(:user, :username => name)
  sign_in_as user
end

# in features/step_definitions/authentication_steps.rb:
Given /^I am not signed in$/ do
  sign_in_as nil
end

module AuthenticationHelpers
  def sign_in_as(user)
    return if @current_user == user
    @current_user = user
    get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
  end
end

World(AuthenticationHelpers)
Roentgenoscope answered 15/3, 2010 at 21:41 Comment(0)
L
2

@Ajedi32 I ran into the same issue (undefined method 'current_session' for Capybara::RackTest::Driver) and putting this in my step definition fixed the problem for me:

rack_test_browser = Capybara.current_session.driver.browser

cookie_jar = rack_test_browser.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
cookie_jar[:stub_user_id] = @current_user.id

In my controller action, I referred to cookies[:stub_user_id], instead of cookie_jar[:stub_user_id]

Leclair answered 5/9, 2012 at 1:51 Comment(0)
F
1

Why don't you use FactoryGirl or (Fixjour or Fabricator) with Devise (or Authlogic) and SentientUser? Then you can simply sniff which user is already logged in!

@user = Factory(:user)       # FactoryGirl
sign_in @user                # Devise
User.current.should == @user # SentientUser
Fireeater answered 8/9, 2011 at 19:50 Comment(0)
B
0

Another slight variation:

# In features/step_definitions/authentication_steps.rb:

class SessionsController < ApplicationController
  def create_with_security_bypass
    if params.has_key? :user_id
      session[:user_id] = params[:user_id]
      redirect_to :root
    else
      create_without_security_bypass
    end
  end

  alias_method_chain :create, :security_bypass
end

Given %r/^I am logged in as "([^"]*)"$/ do |username|
  user = User.find_by_username(username) || Factory(:user, :username => username)
  page.driver.post "/session?user_id=#{user.id}"
end
Batten answered 12/1, 2011 at 10:35 Comment(1)
I get this error! undefined method 'post' for #<Capybara::Selenium::Driver:0x42ad1b4> (NoMethodError). Looks like we can't post with Selenium, at least.Hypocorism
C
0

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'. One such solution is here.

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, one feature is that if the user_name is not 'testuser', then the normal path is taken so they can be tested.

Hope this helps others...

Corbitt answered 11/3, 2013 at 0:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.