Factory Girl / Capybara deleting records from database mid-test?
Asked Answered
G

4

14

Working with RSpec & Capybara, I'm getting an interesting test failure mode which goes away with a few subtle rearrangements of lines in the test case...stuff that shouldn't matter.

I'm developing my own authentication system. It is currently working and I can login/out with the browser and the session works etc etc. However, trying to test this is failing. Something is going on that I don't quite understand, which seems to depend on the order of (seemingly) unrelated calls.

require 'spec_helper'

describe "Sessions" do
  it 'allows user to login' do
    #line one
    user = Factory(:user)
    #For SO, this method hashes the input password and saves the record
    user.password! '2468'

    #line two
    visit '/sessions/index'


    fill_in 'Email', :with => user.email
    fill_in 'Password', :with => '2468'
    click_button 'Sign in'

    page.should have_content('Logged in')
  end
end

As is, that test fails...the login fails. After inserting 'debugger' calls into both the spec and the controller I can see why: the user is not getting inserted into the database as far as the controller is concerned:

Edit adding in ApplicationController

class ApplicationController < ActionController::Base
  helper :all
  protect_from_forgery

  helper_method :user_signed_in?, :guest_user?, :current_user

  def user_signed_in?
    !(session[:user_id].nil? || current_user.new_record?)
  end

  def guest_user?
    current_user.new_record?
  end

  def current_user
    @current_user ||= session[:user_id].nil? ? User.new : User.find(session[:user_id])
  rescue ActiveRecord::RecordNotFound
    @current_user = User.new
    flash[:notice] = 'You\'ve been logged out.'
  end
end


class SessionsController < ApplicationController
  def login
    user = User.where(:email=>params[:user][:email]).first

    debugger ###

    if !user.nil? && user.valid_password?(params[:user][:password])
      #engage session
    else
      #run away
    end
  end

  def logout
    reset_session
    redirect_to root_path, :notice => 'Logget Out.'
  end
end

in the console, at the above breakpoint:

1.9.2 vox@Alpha:~/Sites/website$ rspec spec/controllers/sessions_controller_spec.rb 
/Users/vox/Sites/website/app/controllers/sessions_controller.rb:7
if !user.nil? && user.valid_password?(params[:user][:password])
(rdb:1) irb
ruby-1.9.2-p180 :001 > User.all.count
 => 0 
ruby-1.9.2-p180 :002 > 

However, if I rearrange a few lines in my test, putting line 'two' above line 'one':

describe "Sessions" do
  it 'allows user to login' do
    #line two
    visit '/sessions/index'

    #line one
    user = Factory(:user)
    #For SO, this method hashes the input password and saves the record
    user.password! '2468'


    fill_in 'Email', :with => user.email
    fill_in 'Password', :with => '2468'
    click_button 'Sign in'

    page.should have_content('Logged in')
  end
end

I get this in the console (same breakpoint as above):

1.9.2 vox@Alpha:~/Sites/website$ rspec spec/controllers/sessions_controller_spec.rb 
/Users/vox/Sites/website/app/controllers/sessions_controller.rb:7
if !user.nil? && user.valid_password?(params[:user][:password])
(rdb:1) irb
ruby-1.9.2-p180 :001 > User.all.count
 => 1 

For the sake of brevity I've omitted the full dump of the contents of the user object but I can assure you that the test completes as expected.

This behavior of swapping lines to get the test to pass doesn't really fit well with my idea of what should be going on with these commands and has proven to be quite a bear to my testing in other areas.

Any hints as to what is going on here?

I've scoured google and SO for ideas which present this problem, and there are no shortage of SO questions about RSpec/Capybara and Sessions. Nothing seemed to fit quite right though.

Thanks for looking.

Update

I've added a breakpoint (just before a visit call) and some debugging to the test and come back with this:

(rdb:1) user
#<User id: 1, login_count: 1, email: "[email protected]", encrypted_password: "11f40764d011926eccd5a102c532a2b469d8e71249f3c6e2f8b...", salt: "1313613794">
(rdb:1) User.all
[#<User id: 1, login_count: 1, email: "[email protected]", encrypted_password: "11f40764d011926eccd5a102c532a2b469d8e71249f3c6e2f8b...", salt: "1313613794">]
(rdb:1) next
/Users/vox/Sites/website/spec/controllers/sessions_controller_spec.rb:19
fill_in 'Email', :with => user.email
(rdb:1) User.all
[]

So clearly something along the way that visit does is telling Factory Girl that its done with the user object and so she deletes it?

Edit After inspecting test.log carefully, nothing is issuing any delete. So I'm more or less back to square one.

Grommet answered 12/8, 2011 at 0:29 Comment(0)
G
24

With the help of the Factory Girl mailing list I've found the issue.

By default RSpec uses transactions to maintain the database in a clean state and each transaction is tied to a thread. Somewhere along the pipeline the visit_page command splits off and the transaction tied to the current thread dies.

The solution is simple: disable transactions.

describe "Sessions" do
  self.use_transactional_fixtures = false

   it 'no longer uses transactions' do
     #whatever you want
  end
end

Update for Rails 5.1

As of Rails 5.1, use_transactional_fixtures is deprecated and should be replaced with use_transactional_tests.

self.use_transactional_tests = false
Grommet answered 18/8, 2011 at 19:10 Comment(2)
It's best to be set up in the spec_helper.rb.Congo
This had me stumped all day as I had a lot of distractions that led me down the wrong path. Thanks!Greet
C
3

I think the user variable in RSpec has overwritten the one in the controller so it didn't work ? (couldn't get right user.email in the test)

Before :

user = Factory(:user)
user.password! '2468'

visit '/sessions/index' # user gets overwritten

fill_in 'Email', :with => user.email # can't get user.email

After :

visit '/sessions/index' # Execute action

user = Factory(:user) # user gets overwritten
user.password! '2468'

fill_in 'Email', :with => user.email  # user.email works
Contractual answered 16/8, 2011 at 22:54 Comment(1)
I don't think its an issue with the user variable getting overwritten, but I'll look into it. Thanks.Grommet
M
1

This isn't technically an answer, more of a comment but to clarify the code it's the easiest mechanism.

Can you try doing the following to help narrow down where the user's being destroyed

describe "Sessions" do
  it 'allows user to login' do
    #line one
    user = Factory(:user)
    #For SO, this method hashes the input password and saves the record
    user.password! '2468'


# check the user's definitely there before page load
puts User.first

    #line two
    visit '/sessions/index'

# check the user's still there after page load
puts User.first.reload


    fill_in 'Email', :with => user.email
    fill_in 'Password', :with => '2468'
    click_button 'Sign in'

# check the user's still there on submission (though evidently not)
puts User.first.reload

    page.should have_content('Logged in')
  end
end

EDIT

The fact that it works for you ok in real life but not in Capybara suggests that it may be a product of existing session information. When you're testing in the browser you're usually going off the back of previous work but Capybara is always starting from a clean session.

You can easily see if you can reproduce the Capybara error in-browser by clearing all your cookies (as I'm sure you know) or by just switching to a new incognito window in Chrome/FF which is a nice quick way to get a clean session.

Mulholland answered 16/8, 2011 at 23:13 Comment(4)
Well, thanks for the tip...I know a little more about the problem now, but I still can't get it sorted.Grommet
Can you post the code for the method that responds to visit '/sessions/index', presumably sessions_controller#index (might be worth considering putting this on sessions/new btw) and also your application controller.Mulholland
Now you've identified the problem as happening in the sessions index then just using those puts User.last debugging points around the code should let you zoom into exactly where the user is getting deleted. I don't think it's FactoryGirl taking it out BTW - I don't think FactoryGirl can delete stuffMulholland
sessions/index is just the view, and its just a form. No controller action is executed. I've filled out some more relevant parts of the sessions_controller#login and #logout methods and added in ApplicationController. Nothing funky going on in there.Grommet
G
0

The correct answer above helped me. Of course, I needed to change some other tests that (wrongly or rightly) assumed a fixture did not exist. For more information: there's some info about this in the Capybara README.

https://github.com/jnicklas/capybara

"If you are using a SQL database, it is common to run every test in a transaction, which is rolled back at the end of the test, rspec-rails does this by default out of the box for example. Since transactions are usually not shared across threads, this will cause data you have put into the database in your test code to be invisible to Capybara."

You can also config RSpec to clean up after your test manually:

https://github.com/jnicklas/carrierwave/wiki/How-to%3A-Cleanup-after-your-Rspec-tests

Greet answered 15/4, 2013 at 1:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.