Replacing Selenium with Cuprite for Rails system tests
In the world of tests, system tests are as close to real users as it gets: you control a browser and make it use your app as you expect humans to.
To do so, you need two things:
- a browser : you will see it (safari, chrome…) run on its own on your machine unless you set it to be "headless", a gory way to tell your machine that you actually don't want to see the insides of your tests
- a driver : the pilot that will take your test instructions and command the browser
By default, Rails ships with Selenium, but there's a new kid in town: Cuprite, a "Headless Chrome driver for Capybara", which is faster and has some nice tricks up its sleeves like options to pause and debug.
I discovered cuprite thanks to Evil Martians blog post: "**System of a test:** Proper browser testing in Ruby on Rails".
The following is just a quick-start version for those who don't care about docker or rspec, with some minor twists.
Basic setup
To get started, you simply need to add the cuprite gem. Selenium will have to stay unless you're on Rails 6.1.
group :test do
gem 'capybara'
gem 'selenium-webdriver' # Only for rails <= 6.1
gem 'cuprite'
end
And then you can edit test/application_system_test_case.rb
require "test_helper"
require "capybara/cuprite" # <- Add this
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# And replace selenium with cuprite
driven_by :cuprite, using: :chromium, screen_size: [1400, 1400]
end
And with this you're good to go.
You'll notice that I'm not using :chrome
but the open source browser it's based on: Chromium. This is because I just uninstalled Chrome, because https://chromeisbad.com.
https://twitter.com/sowenjub/status/1338958371844726784?s=20
A more elaborate setup
There are a couple more setup files I grabbed from Evil Martians' setup, with a slightly different organisation since I'm not using rspec.
test/
test_helpers/
system/
better_rails_system_tests.rb
capybara_setup.rb
cuprite_helpers.rb
cuprite_setup.rb
And with this, test/application_system_test_case.rb
will end up looking like this:
require "test_helper"
require "test_helpers/system/better_rails_system_tests"
require "test_helpers/system/capybara_setup"
require "test_helpers/system/cuprite_helpers"
require "test_helpers/system/cuprite_setup"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :cuprite, using: :chromium, screen_size: [1400, 1400]
include BetterRailsSystemTests
include CupriteHelpers
end
And of course you need to create the files, taken from Evil Martians.
# test_helpers/system/better_rails_system_tests
module BetterRailsSystemTests
# Use our `Capybara.save_path` to store screenshots with other capybara artifacts
# (Rails screenshots path is not configurable https://github.com/rails/rails/blob/49baf092439fc74fc3377b12e3334c3dd9d0752f/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb#L79)
def absolute_image_path
Rails.root.join("#{Capybara.save_path}/screenshots/#{image_name}.png")
end
# Make failure screenshots compatible with multi-session setup.
# That's where we use Capybara.last_used_session introduced before.
def take_screenshot
return super unless Capybara.last_used_session
Capybara.using_session(Capybara.last_used_session) { super }
end
end
# test_helpers/system/capybara_setup
# Usually, especially when using Selenium, developers tend to increase the max wait time.
# With Cuprite, there is no need for that.
# We use a Capybara default value here explicitly.
Capybara.default_max_wait_time = 2
# Normalize whitespaces when using `has_text?` and similar matchers,
# i.e., ignore newlines, trailing spaces, etc.
# That makes tests less dependent on slightly UI changes.
Capybara.default_normalize_ws = true
# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).
# It could be useful to be able to configure this path from the outside (e.g., on CI).
Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara")
# The Capybara.using_session allows you to manipulate a different browser session, and thus, multiple independent sessions within a single test scenario. That’s especially useful for testing real-time features, e.g., something with WebSocket.
# This patch tracks the name of the last session used. We’re going to use this information to support taking failure screenshots in multi-session tests.
Capybara.singleton_class.prepend(Module.new do
attr_accessor :last_used_session
def using_session(name, &block)
self.last_used_session = name
super
ensure
self.last_used_session = nil
end
end)
For this one I created a dedicated file but you could put it at the end of cuprite_setup
as well.
# test_helpers/system/cuprite_helpers
module CupriteHelpers
# Drop #pause anywhere in a test to stop the execution.
# Useful when you want to checkout the contents of a web page in the middle of a test
# running in a headful mode.
def pause
page.driver.pause
end
# Drop #debug anywhere in a test to open a Chrome inspector and pause the execution
def debug(*args)
page.driver.debug(*args)
end
end
# test_helpers/system/cuprite_setup
# First, load Cuprite Capybara integration
require "capybara/cuprite"
# Then, we need to register our driver to be able to use it later
# with #driven_by method.
Capybara.register_driver(:cuprite) do |app|
Capybara::Cuprite::Driver.new(
app,
**{
window_size: [1200, 800],
# See additional options for Dockerized environment in the respective section of this article
browser_options: {},
# Increase Chrome startup wait time (required for stable CI builds)
process_timeout: 10,
# Enable debugging capabilities
inspector: true,
# Allow running Chrome in a headful mode by setting HEADLESS env
# var to a falsey value
headless: !ENV["HEADLESS"].in?(%w[n 0 no false])
}
)
end
# Configure Capybara to use :cuprite driver by default
Capybara.default_driver = Capybara.javascript_driver = :cuprite
And now we just have to write a simple test:
require "application_system_test_case"
class HomeTest < ApplicationSystemTestCase
test "open home screen" do
visit root_url
save_and_open_page # Will save the html page in /tmp/capybara and open it in your default browser
take_screenshot # Will save a screenshot in /tmp/capybara/screenshots
pause # To see the current view, requires HEADLESS=0 (or n, no, false)
debug() # To see the current view with debug tools
assert_selector "h1", text: "Hello, world"
end
end
To launch it, run the following:
# If you want to see the browser or use `pause`
HEADLESS=0 rails test test/system/home_test.rb
# Otherwise this will do
rails test test/system/home_test.rb