This past week I worked on converting a Rails and Angular application that was using Selenium as the driver for Spinach/Capybara tests over to using Poltergeist.
The change was motivated by the fact that our Spinach tests using Selenium where taking an average of 4 minutes to run. That's not terrible, but could definitely be improved. Also, I wanted to look into running tests in parallel which would be easier with a headless driver.
Spoiler alert: 2x speed improvement ahead.
Getting PhantomJS Running
When I first switched our tests over to using Poltergeist I noticed that even though links where being clicked ui-router was not transitioning to the new state.
After some digging I realized the issue was that PhantomJS does not support Function.prototype.bind in 1.9.x versions and our app had code in the resolve section of our states was using bind
. When calling bind
failed the state transition would be halted.
The solution is to add a polyfill such as this bower package which can be installed using Rails Assets.
# Gemfile
source 'https://rails-assets.org'
gem 'rails-assets-bind-polyfill'
# application.js
//= require bind-polyfill
Throwing Errors
By default Poltergeist does not re-raise errors, however, having this turned on is usually helpful in finding errors. Angular makes this a challenge though: instead of throwing errors it catches them using the $exceptionHandler
service and logs them. If we want to be able to re-raise errors we will need to load an extension for Poltergeist.
# features/support/env.rb
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(
app,
extensions: [ 'features/support/angular_errors.js' ],
js_errors: true
)
end
In our extension we can redefine the error
function on the $log
service to instead throw an exception. Original script taken from the Localytics blog.
I also added void implementations for $log.info
and $log.debug
to clean up test output as our app logs every state transition with debugging information.
// features/support/angular_errors.js
window.onload = function() {
var $injector = angular.element(document).injector();
var $log = $injector.get('$log');
// Raise Angular errors
$log.error = function(error) { throw(error); };
// Suppress Angular Logging
$log.info = function () {};
$log.debug = function () {};
};
Debugging
One thing that's always hard with a headless browser is to see what's going on when the test is interacting with the page. For basic debugging, I've found that taking a screenshot using the save_screenshot
method while in a Pry session of stepping through your tests works well.
save_screenshot('/Users/caleb/Desktop/debug.png', full: true)
For debugging tests that don't have driver specific issues I still like to use Selenium so that you can click around the page. So I created a hook to switch the driver any time the @selenium
tag is added to a scenario.
Spinach.hooks.on_tag('selenium') do
Capybara.current_driver = :selenium
end
2x Speed Improvement
After fixing a couple of tests that used features of Capybara specific to Selenium and some issues with mouse events, I was able to run the entire test suite with Poltergeist.
With these changes in place running Spinach tests, which took 4 minutes using Selenium, now took just 2 minutes. Well worth the effort.
Edit 2015-02-24: Today after upgrading PhantomJS to version 2.0.0 I'm seeing even more remarkable speed improvements. The same Spinach tests now run in about 75 seconds. Overall a 3.2x improvement from using Selenium.