Code to nowhere

Blog :: N. T. Rutherford

18 notes &

Rails / RSpec / Capybara / Selenium transactional tests pitfall, redux

The following discusses a setup quirk and work-around for RSpec request specs built with Capybara.

Scenario

You have some test code like this:

specify "clicking through to Paypal via the website" do
  Configuration['Paypal Saved Button ID'] = "xxxxxxxxxx"
  visit new_member_path
  fill_in("member[name]", :with => "Ben")
  fill_in("member[email]", :with => "bengnn@gmail.com")
  click_on("member_submit")

  page.should have_content("Your account is currently pending payment, and you cannot yet access the member facilities.")
  page.should have_content("You will gain full access to the site when we have received notification of your payment from Paypal.")
  click_on "paypal_payment_button"
  URI.parse(current_url).host.should == "www.paypal.com"
  URI.parse(current_url).path.should == "/uk/cgi-bin/webscr"
  page.should have_content("membership for Ben (bengnn@gmail.com)")
end

and you’re going crazy trying to figure out why the Paypal button is missing the critical hosted_button_id parameter which you just set in the test code. As you start debugging you’ll notice that the test code sees the data, but the app code does not.

Why

An old, well known, but easily forgotten, issue in cucumber @javascript testing is rails’s database transaction functionality. Each unit test runs in its own transaction, allowing them to be nicely isolated from one another, and to quickly clean-up the mess they make when they finish by aborting the transaction.

There is a snag, however: each thread of execution has its own database connection. When Capybara is using the default :rack_test driver everything is running in the same thread, so it just works. When you switch to the :js (:selenium) driver you get a webserver running in a different thread to the one of your test code, meaning they see different transactions, and so do not share the state you set up in your test. The database state you are setting up in your test code is not the database state which your test will execute against. So in the above example the Paypal button id will be nil, not the value set in the test.

Solutions

3 approaches to fixing this:

  • Don’t use transactional features anywhere (results in slower tests)
  • Use transactional features only for rack-test (like with Cucumber, but this can stop working at version changes, and is still a little slower)
  • Use transactional features everywhere, and tell rails to use a single shared database connection (only for the test environment)

Conventional / Cucumber logic is to use Ben Mabey’s database_cleaner to truncate the database with something along these lines dropped in as a spec/support file:

RSpec.configure do |config|
  config.use_transactional_fixtures = false

  config.before :each do
    if Capybara.current_driver == :rack_test
      DatabaseCleaner.strategy = :transaction
    else
      DatabaseCleaner.strategy = :truncation
    end
    DatabaseCleaner.start
  end

  config.after do
    DatabaseCleaner.clean
  end
end

Thanks to Jo Liss for the info & above snippet.

This is a little slower than transactional tests, and adds a breakable test dependency. It turns out there’s another option, which is to tell Rails to use a single, shared, database connection. Of course, you just want this for the test environment, but that’s easy to do in various ways. My preference is for plug-in spec/support files, for example putting José Valim’s snippet into spec/support/shared_db_connection.rb:

class ActiveRecord::Base
  mattr_accessor :shared_connection
  @@shared_connection = nil

  def self.connection
    @@shared_connection || retrieve_connection
  end
end

# Forces all threads to share the same connection. This works on
# Capybara because it starts the web server in a thread.
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection

Capybara group discussion with explanations

Filed under capybara rspec rails testing missing data records

  1. nruth posted this