Racking my brains
I'm using Cucumber and Capybara for integration testing on a web application that depends heavily on the use of subdomains. Since some features rely on client-side Javascript, some scenarios use the Selenium 2 (WebDriver) driver, while other scenarios use the regular rack-test driver.
The Selenium driver does a great job of handling subdomains, and switching subdomains in the middle of a scenario. Setting Capybara.app_host
is all that's needed, with a couple of caveats:
- You include the port number of your Capybara server
- Your
/etc/hosts
has the relevant subdomains defined
Since I'm using Capybara 0.4.0.rc, I've specified which port to use in features/support/custom_env.rb
(you could also put this in features/support/env.rb
, but since that's overwritten when you upgrade, it's best to keep your customizations separate):
# custom_env.rb Capybara.run_server = true Capybara.server_port = 9887
Then, when I need to switch subdomains in a step definition:
# features/step_definitions/general_steps.rb DOMAIN = "example.com" PORT = Capybara.server_port Given /^I am in subdomain "(.+)"$/ do |subdomain| Capybara.app_host = "#{subdomain}.#{DOMAIN}:#{PORT}" end
This works well enough if all your scenarios are tagged @selenium
, but what happens if you try to switch subdomains using the default rack-test driver? It doesn't work, because the rack-test driver that comes with Capybara doesn't look at app_host
, only default_host
. And what's worse, it only uses that when the first rack-test mock session is created:
# lib/capybara/driver/rack_test_driver.rb def build_rack_mock_session @mock_session = Rack::MockSession.new(app, Capybara.default_host || "www.example.com") end
Any on-the-fly changes to Capybara.default_host
will have no effect, since the session has already been created. The net effect is that your first scenario might use the correct subdomain, then you're stuck in that subdomain--and since rack-test runs rather silently in an invisible mock browser, the only symptom of this may be strange errors about nil
objects and missing page content. Some scenarios might run fine in isolation (if they only use one subdomain), but then blow up when you try to run them as part of a larger batch (two scenarios, each using a different subdomain).
After failing at a bunch of different workarounds, I was ready to give up and just stick @selenium
tags on everything (effectively tripling Cucumber's execution time). Just as I was preparing to leap from the precipice, a co-worker pointed me towards Tristan Dunn's article Multiple Sessions in Cucumber & Selenium. This technique was designed to allow multiple Selenium browser instances to be in different subdomains, by creating separate sessions for each one. Since my problem with rack-test was directly related to being unable to create a new session for each subdomain, this proved to be an excellent starting point.
I mainly wanted the ability to switch sessions whenever the subdomain changed, so I factored out a new method switch_session
:
# features/support/session.rb module Capybara module Driver module Sessions def set_session(id) Capybara.instance_variable_set('@session_pool', { "#{Capybara.current_driver}#{Capybara.app.object_id}" => $sessions[id] }) end def switch_session(id) $sessions ||= {} $sessions[:default] ||= Capybara.current_session $sessions[id] ||= Capybara::Session.new(Capybara.current_driver, Capybara.app) set_session(id) end def in_session(id, &block) switch_session(id) yield set_session(:default) end end end end World(Capybara::Driver::Sessions)
Now, changes to Capybara.default_host
will be picked up whenever a new subdomain is used for the first time. When I switch to subdomain foo
, then $sessions['foo']
stores a new Capybara::Session
. If I switch to a different subdomain, then back to foo
later on, it'll just use the same foo
session created earlier.
And now my subdomain-switching step looks like this:
# features/step_definitions/general_steps.rb Given /^I am in subdomain "(.+)"$/ do |subdomain| if Capybara.current_driver == :selenium Capybara.app_host = "#{subdomain}.#{DOMAIN}:#{PORT}" else Capybara.default_host = "#{subdomain}.#{DOMAIN}" switch_session(subdomain) visit("http://#{subdomain}.#{DOMAIN}") end end
At first, this worked well, except my Selenium scenarios kept opening new browser windows. Since I don't really need multiple-session support in my Selenium tests at this point, I solved this by using a single session for all Selenium-driven tests:
# features/support/custom_env.rb Before('@selenium') do switch_session('selenium') end
Now, my Selenium and rack-test scenarios can coexist, with reliable subdomain behavior. Success!