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