๐ Updated March 2024
Test all of your Rails app's functionality even at the highest feature level right in RSpec with the rest of your unit and integration tests.
Use RSpec Rails system specs to test your app's functionality at the
web server (e.g. Puma) level.
Consider using RSpec Rails system specs and test doubles to reduce your
End-to-End testing in a test environment.
This is an overview and recipe for adding RSpec Rails system specs to your app's RSpec
test suite. Here you will get...
- The background on
systemspecs - Their gem dependencies and how to add them
- An overview of configuring Capybara
- An overview of writing
systemspecs and how to run them
RSpec Rails system specs
are the only level of RSpec Rails tests that launch the web server
(Puma by default).
system specs...
- Rely on and require Capybara gem
- Use database transactions (which are rolled back) so the DatabaseCleaner gem is NOT needed
Rails (Minitest) introduced system tests
in Rails 5.1 to build in the ability to functionally test your Rails application at
the web browser level with Capybara.
system specs are RSpec Rails' wrapper for the Rails' (Minitest) system tests.
By default, system tests are run with the Selenium driver, using the Chrome browser, and a screen size of 1400x1400.
RSpec does not use your
ApplicationSystemTestCasehelper. Instead it uses the defaultdriven_by(:selenium)from Rails.
At a minimum, your system specs require...
gem 'capybara'gem 'selenium-webdriver'
By default, Rails (i.e. rails new myapp) should automatically include Minitest
system test dependencies in the generated Gemfile.
However, IF you skipped adding Minitest when you generated your app
(e.g. rails new myapp -T) and added RSpec Rails manually to your project and Gemfile,
THEN you will need to add the system spec dependencies manually as well.
Ensure that you have in your app's Gemfile...
RSpec-Rails
group :development, :test do
...
gem 'rspec-rails'
...
endCapybara and Selenium Webdriver
group :development, :test do
...
# Adds support for RSpec system specs
gem 'capybara'
gem 'selenium-webdriver'
...
endIF you manually added gems to the Gemfile, THEN you will need to install
them with bundle install.
By default Rails Minitest uses and configures the ApplicationSystemTestCase helper
in test/application_system_test_case.rb...
require 'test_helper'
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
endHowever, as mentioned, RSpec does not use your ApplicationSystemTestCase helper!
By default, RSpec Rails uses the Rails default driven_by(:selenium).
Your specific Rails app's Capybara configuration depends on your intended supported browsers and whether they are local or remote. However, I have included a sample Capybara configuration that I use to demonstrate its configuration. Since this is at the web server level, you must set the IP and Port of the host/webserver for your app.
I like to have my development (and other) environment(s) as close to the intended production environment as possible to reduce the risk of surprises later. Running your Rails app in containers and containerized dev environments is a great way to do this. I also recommend using Selenium's Docker Containers to simplify browser dependencies.
๐ If you are using an ARM-based Mac (e.g. M2 chip), you will need to use the forked Docker Seleniarm Containers
This sample Capybara implementation supports both remote browsers (i.e. Selenium/Seleniarm Containers) and local/native browsers.
You implement this by...
- Creating a
spec/support/capybara.rbconfiguration file - Requiring the capybara configuration file in
spec/rails_helper.rb
This implementation uses environment variables to supply the running environment-specific values for...
PORT- the port for your Rails App (e.g.PORT="3000")BROWSER- the type of Selenium-supported browser (e.g.BROWSER=chrome)HEADLESS- if set (including set to empty), specifies running in headless mode provided that the specified browser supports it (e.g.HEADLESS=)SELENIUM_REMOTE- the remote browser URL (e.g.SELENIUM_REMOTE='http://localhost:4444/wd/hub')CAPYBARA_WAIT- the time in seconds for Capybara to wait on page URLs or elements to load
Note that when running locally/natively, if BROWSER is not set,
this example will default to the :selenium browser which is Chrome.
In your Rails app's project directory, create file
spec/support/capybara.rb with the following Capybara configuration...
# frozen_string_literal: true
def create_capybara_browser(browser:, url:)
return :selenium unless browser
Capybara.register_driver :capybara_browser do |app|
options = browser_options(browser)
Capybara::Selenium::Driver.new(
app,
browser: url ? :remote : browser.to_sym,
options: options,
url:)
end
:capybara_browser
end
def browser_options(browser)
browser = browser.to_s.gsub(/\W/, '').capitalize
# e.g. Selenium::WebDriver::Chrome::Options.new
options = Selenium::WebDriver.const_get(browser).const_get('Options').new
options.add_argument('--headless') if headless_specified?
options
end
def headless_specified?
headless = ENV.include?('HEADLESS')
end
def configure_rspec_capybara(capybara_browser, app_host_port)
# This is always running locally (even in container) so get local IP address
app_host = IPSocket.getaddress(Socket.gethostname)
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by capybara_browser
Capybara.app_host = "http://#{app_host}:#{app_host_port}"
Capybara.server_host = app_host
Capybara.server_port = app_host_port
end
end
end
# -- Main Capybar Configuration --
url = ENV['SELENIUM_REMOTE']
browser = ENV['BROWSER']
capybara_browser = create_capybara_browser(browser:, url:)
configure_rspec_capybara(capybara_browser, ENV['PORT'])
# Increase the Capybara Wait Time
# NOTE: Docker Compose can convert environment variables values to string
Capybara.default_max_wait_time = ENV.fetch('CAPYBARA_WAIT', 10).to_iYou add the Capybara configuration you just created to the RSpec Rails framework
by requiring it in your app's spec/rails_helper.rb file by adding the following line...
require 'support/capybara'Once you have your Capybara configuration implemented as you like, you can start writing and running your system specs.
By convention in RSpec Rails, you put your system specs under the spec/system directory.
You can also explicitly tag your system specs with :type => :system...
RSpec.describe 'Hello Rails', type: :system do
...
endYou can use rails generate to generate your system specs...
bundle exec bin/rails generate rspec:system [test-name]
For example bundle exec bin/rails generate rspec:system hello_rails.
Here is a very simple example of an RSpec Rails system spec expecting the phrase "Hello Rails" to be on the home page of the Rails app...
# Specifies user sees Hello Rails on home page
require 'rails_helper'
RSpec.describe 'Hello Rails', type: :system do
context 'when user visits the Home Page...' do
before(:each) do
visit '/'
end
it { expect(page).to have_text('Hello, Rails!') }
end
endBe sure to specify any environment variables.
Here's an example of running all the RSpec tests with the Sample Remote Browser (Container) Capybara Implementation...
SELENIUM_REMOTE='http://localhost:4444/wd/hub' \
BROWSER=chrome \
APP_HOST_PORT="3000" \
bundle exec rspec
Run your Rails RSpec tests the way that you normally do to include your system specs, for example...
bundle exec rspec
bundle exec rake spec
You can also run just your RSpec Rails system specs using...
-
Rails rake task
rake spec:systembundle exec rake spec:system -
RSpec's abilities to specify tests to run your
systemspecs...-
To run only specs in the
spec/systemfolder...bundle exec rspec spec/system -
To run only
type: :systemtagged system specs...bundle exec rspec --tag type:system
-
You can run all of your tests except for your RSpec Rails system specs using RSpec's exclude functions...
-
To exclude specs in the
spec/systemfolder...bundle exec rspec --exclude-pattern "spec/system/*_spec.rb" -
To exclude
type: :systemtagged system specs...bundle exec rspec --tag ~type:system
I used some of the Capybara configuration from my personal projects which I've refined over the years.
When I learned and implemented this, I was specifically doing it in containers and in Kubernetes. These sources reflect that.