🕐 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
system
specs - Their gem dependencies and how to add them
- An overview of configuring Capybara
- An overview of writing
system
specs 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
ApplicationSystemTestCase
helper. 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'
...
end
Capybara and Selenium Webdriver
group :development, :test do
...
# Adds support for RSpec system specs
gem 'capybara'
gem 'selenium-webdriver'
...
end
IF 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]
end
However, 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.rb
configuration 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_i
You 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
...
end
You 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
end
Be 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:system
bundle exec rake spec:system
-
RSpec's abilities to specify tests to run your
system
specs...-
To run only specs in the
spec/system
folder...bundle exec rspec spec/system
-
To run only
type: :system
tagged 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/system
folder...bundle exec rspec --exclude-pattern "spec/system/*_spec.rb"
-
To exclude
type: :system
tagged 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.