A Quick Guide to Rails System Tests in RSpec
Just this week, RSpec 3.7 was released with support for the Rails system tests added in Rails 5.1.
(If you’d like to read more about system tests and see examples of them in action, my book Rails 5 Test Prescriptions is now avaiable for purchase)
What are System tests?
System tests were added to Rails core in Rails 5.1 as the core team’s preferred way to test client-side interactions using Capybara and a browser driver. This is in addition to the Rails core integration tests, which don’t use Capybara, but which do test against server-side generated DOM elements.
Okay, but I already use Capybara in my RSpec tests, why is this better?
Even if you are already using RSpec feature tests with Capybara, the system tests still offer some benefits:
Configuring the Capybara browser is easier, especially in the default case.
If you’ve used Capybara to drive JavaScript testing in an external browser, you may have run into issues where the database did not clean up as expected. Or if you were using SQL lite, the database would sometimes lock in Capybara JavaScript testing. These problems would happen because the external driver would run in a separate process and would not have access to the database transactions or locks being used by the Rails app. You may have used the Database cleaner gem to mitigate this problem. Rails system tests run the driver in the Rails process, so that these problems go away.
So, should I use System Tests?
The RSpec team is recommending using system tests instead of feature tests for integration testing, whether or not you are using a JavaScript driver.
How do I use system tests?
In RSpec, you can define a system test using metadata and the :type
keyword, as in
RSpec.describe "add to cart", type: :system do
If you have the infer_spec_type_from_file_location!
configuration turned on, then any spec in the spec/system
directory is a system test.
How do I set up System tests?
System tests use a method called driven_by
, which is part of Rails core, and which manages the Capybara driver configuration.
The argument to driven_by
is the Capybara driver. The default is :selenium
, but you can also use :rack_test
, :selenium_chrome
, or :selenium_chrome_headless
.
The rack_test
driver is the normal Capybara default, it uses an internal DOM tree and does not support JavaScript. System tests use the non-headless :selenium
as a default to give JavaScript support and to allow the tester to see the browser going though the tests.
The driven_by
method takes three optional keyword arguments, :using
, which specifies the browser for selenium, as in driven_by :selenium, using: :firefox
, and :screen_size
, which specifies the size of the browser window as a two-element array: driven_by :selenium, screen_size: [640, 480]
. There’s also an :options
keyword which is passed to the driver if the driver requires other options.
You can call driven_by
as part of each test in a before block, but in RSpec you can also make that part of the suite-wide configuration. Here’s the setup I’ve been using:
Using selenium_chrome_headless
requires the selenium-webdriver
gem, which is installed by default in Rails 5.1.
I’m pretty sure that using the selenium_chrome_headless
driver also requires having Chromium installed on your system, but honestly, at this point, I’m not 100% sure beyond the fact that I have it installed on my system and system tests work…
You can install other drivers if you want you’d just require them in the same file that has the driven_by
setup and then use driven_by :poltergeist
or :webkit
(if you use capybara-webkit
).
How hard is the transition?
Pretty easy so far, if you are on Rails 5.1… I’ve moved two small projects by just changing their directory and type, and adding the setup above. Since everything is going back to the same Capybara functionality, everything just worked (though Circle CI had to be coaxed, more on that in a second).
Update: One transition bit that I forgot. If you are using Devise’s integration test helpers like sign_in
you need to add configration to include them, such as: config.include Devise::Test::IntegrationHelpers, type: :system
. Or maybe Devise::Test::ControllerHelpers
depending on which one you are using.
If you are doing something unusual with your Capybara driver, you may need to translate the driver configuration, but the existing Capybara driver creators should work.
That said, I’m not sure that you all need to run out and do this to existing projects unless you are having issues. Getting rid of Database Cleaner does seem like a positive step in making tests less confusing.
If you are using Circle CI 2.0
As far as I can tell (the official docs are lacking), the preferred way to do Selenium in Circle CI 2.0 is via a separate Docker image.
In the spirit of “I spent two hours on this so you don’t have to”, here’s the configuration that works for me.
In my CircleCI config.yml
file, the docker setup looks like this:
The RSpec step is as normal, using the syntax that Circle seems to prefer:
That sets up a SELENIUM_DRIVER_URL
environment variable, which we can then use to force a remote driver in that environment. My actual driver setup looks like this:
So for JavaScript tests, if I am in the Circle environment, as specified by ENV["SELENIUM_DRIVER_URL"].present?
, I use a regular selenium driver, setting the options to mark it as remote, and using the external URL.
It’s still simpler than the Capybara config was before I switched to system testing…
Hope this helps, happy testing!
(If you’d like to read more about system tests and see examples of them in action, my book Rails 5 Test Prescriptions is now avaiable for purchase)