To Selenium or Not To Selenium?
I've been using Capybara with the Selenium driver for automated testing of a Rails project via Cucumber. Certain scenarios involve dealing with client-side Javascript (such as confirming a popup message before deleting a record), which is the reason I started using the Selenium driver. But I soon discovered that Selenium is slow, taking three or four times as long as the standard Rack-test driver. This pushed me to find non-Selenium ways of testing those features.
Web applications often involve working with a list of records that can be created, edited, and deleted. A common way of presenting these records is in an HTML table, with one row for each record, something like this:
If I want Capybara to interact with these, I need a way to click on the "Edit" or "Delete" link for a specific row. This can be accomplished with a bit of XPath:
def xpath_row_containing(a, b) "//table/*/tr[contains(., '#{a}') and contains(., '#{b}')]" end
This expression looks for a table row that contains two specific bits of text, and is pretty generic. I want to use it for clicking the "Delete" link next to "Bar", so I'll write a step definition like this:
When /^I follow "(.+)" next to "(.+)"$/ do |link, identifier| row = find(:xpath, xpath_row_containing(link, identifier)) row.click_link(link) end
Then I can write a step like this:
When I follow "Delete" next to "Bar"
Now, let's say my application displays a popup message using Javascript, confirming that I want to delete the record. How do I deal with that? Well, it's things like this that prompted me to start using Capybara and Selenium in the first place. Unfortunately, as of this writing, Selenium WebDriver has no support for Javascript popups. The issue has been open for over 3 years with no sign of resolution, so it's down to finding an ugly workaround, and this particular workaround is the least ugly one I've seen. Basically, instead of dealing with the popup, I override the builtin confirm
function to just return true:
When /^I confirm the next popup$/ do page.evaluate_script("window.confirm = function(msg) { return true; }") end
Now my deletion-steps can be something like this:
When I confirm the next popup And I follow "Delete" next to "Bar" Then I should see "Bar has been deleted"
Yeah, so the sequence is screwy, but that's OK because I'm gonna hide this in a step definition pretty soon, so my scenario steps don't have to be so ugly.
This takes care of the Selenium-oriented steps. But I have a lot of scenarios dealing with deleting records, and I don't want to use Selenium for all of them. Again, the Selenium driver is slow; 10 minutes of rack-test scenarios becomes 30 or 40 minutes of Selenium scenarios, and nobody wants to wait 40 minutes for integration tests to run before every commit and merge. Since I'm just avoiding the popup anyway, I might as well invoke the deletion directly.
My application uses REST (as every good web application should), so I have routes something like this:
blah GET /blah/:id {:controller=>"blah", :action=>"show"} PUT /blah/:id {:controller=>"blah", :action=>"update"} DELETE /blah/:id {:controller=>"blah", :action=>"destroy"}
In other words, that "Delete" link just sends a DELETE
request to /blah/:id
, where :id
is the primary key of whatever record is being deleted. All I gotta do is find the relevant delete link, then send the DELETE
request directly. I'll use XPath again here:
def delete_link(record_name) "//table/*/tr[contains(., '#{record_name}')]/*/a[contains(., 'Delete')]" end
I know, XPath looks a little ugly, but it is awesome. This XPath expression finds a table row that contains the record name, and looks within it for a link that says "Delete". Now I can do this:
link = find(:xpath, delete_link(record_name)) page.driver.delete(link[:href])
Note that only the Rack-test driver includes the delete
method; Selenium doesn't have it, because Selenium is supposed to mimic a user's actions within the browser, and users can't send DELETE
requests explicitly.
So now I have two ways of doing the same thing--one with Selenium, another without. What if I want my scenario to work with both drivers? Wrap the whole thing in a step definition--I can make a generic step for deleting any record via its "Delete" link:
When /^I delete record "(.+)"$/ do |record_name| if Capybara.current_driver == :selenium When %{I confirm the next popup} And %{I follow "Delete" next to "#{record_name}"} else link = find(:xpath, delete_link(record_name)) page.driver.delete(link[:href]) # I need to follow a redirection at this point -- your app may not require this When %{I follow "redirected"} end Then %{I should see "#{record_name} has been deleted"} end
Now, regardless of whether my scenario is using Selenium or not, I can delete a record with a simple step like this:
When I delete record "Bar"
This gives me the ability to switch to Selenium when I want to debug my steps, then switch back to Rack-test later when I have a need for speed.