Replacing Selenium with Cuprite for Rails system tests

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.


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
Arnaud Joubay

Arnaud Joubay

Swift & Rails Indie Maker. Original Parasider. Time Knight. 11y Remote/WFH. Half of @teambkry. Creator of cows in App Store featured @nomeat_today. Scuba diver.๐Ÿ‘จโ€๐Ÿ’ป๐Ÿ•๐Ÿ‹๐Ÿฅ๐Ÿฎ๐Ÿ‰๐ŸŒฑ.