Four Key Reasons to Learn Markdown
Back-End Leveling UpWriting documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Rails 5.1 was a big step forward in terms of building rich JavaScript frontends in Rails. It included Webpacker, a build pipeline that allows using the latest and greatest JavaScript language features and frameworks, as well as system tests, which can simulate user interaction in a real browser. Many Rails developers use RSpec for testing, and RSpec also includes support for system tests—but the information on how to use them is a bit dispersed.
To help with this, let’s walk through an example of using RSpec system tests. We’ll set up a Rails app using Webpacker, then set up an RSpec system test that exercises our app, including our JavaScript code, in Chrome.
Create a new Rails project excluding Minitest and including Webpacker configured for React. Webpacker can be preconfigured for a number of different JavaScript frameworks and you can pick whichever you like, or even vanilla JavaScript. For the sake of this tutorial, we’ll use React; we won’t be touching any React code, but this demonstrates that this testing approach works with React or any other frontend framework.
$ rails new --skip-test --webpack=react rspec_system_tests
Webpacker organizes your code around the concept of “packs,” root files that are bundled into separate output JavaScript files. Webpacker will set up a sample pack for us in app/javascript/packs/hello_react.jsx
, but it isn’t used in our app automatically. To use it, we need to create a route that’s not the default Rails home page, then add it to our layout.
Add a root route in config/routes.rb
:
Rails.application.routes.draw do # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + root to: 'pages#home' end
Create the corresponding app/controllers/pages_controller.rb
:
class PagesController < ApplicationController end
We don’t need to define a #home
action on that controller, because when Rails attempts to access an action that isn’t defined, the default behavior will be to render the corresponding view. So let’s just create the view, app/views/pages/home.html.erb
and put some text in it:
Hello Rails!
Now, to get our hello_react
pack running, let’s add it to the head of app/views/layouts/application.html.erb
:
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + <%= javascript_pack_tag 'hello_react' %> </head>
Run your server:
$ rails s
Load http://localhost:3000
in a browser and you should see:
Hello Rails! Hello React!
So our React pack is working. Great!
Now let’s get it tested. Add rspec-rails
and a few other gems to your Gemfile
:
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'rspec-rails' end +group :test do + gem 'capybara' + gem 'selenium-webdriver' +end
rspec-rails
should be added to both the :development
and :test
groups so its generators can be run from the command line. capybara
provides test methods for us to simulate users interacting with our app, and selenium-webdriver
lets us interact with real browsers to run the tests.
Ask Rails to set up the necessary RSpec configuration files:
$ rails generate rspec:install
You’ll also need to install chromedriver
, a tool for running Google Chrome in tests. Download Chromedriver or install it with Homebrew:
$ brew tap caskroom/cask $ brew cask install chromedriver
Now we’re ready to write our test! In older versions of RSpec, the tests that simulated user interaction with Capybara were called feature tests, but now that Rails has built-in system testing functionality, it’s recommended to use RSpec system tests to use that same testing infrastructure under the hood.
Generate a system test:
$ rails generate rspec:system hello_react
This creates the humorously-pluralized hello_reacts_spec.rb
with the following contents:
require 'rails_helper' RSpec.describe "HelloReact", type: :system do before do driven_by(:rack_test) end pending "add some scenarios (or delete) #{__FILE__}" end
Replace the pending
line with our test:
- pending "add some scenarios (or delete) #{__FILE__}" + it 'should render a React component' do + visit '/' + expect(page).to have_content('Hello React!') + end end
Run the test:
$ bundle exec rspec
Oh no, it fails! Here’s the error:
Failures: 1) HelloReact should render a React component Failure/Error: expect(page).to have_content('Hello React!') expected to find text "Hello React!" in "Hello Rails!"
It looks like our test is seeing the “Hello Rails!” content rendered on the server in our ERB file, but not the “Hello React!” content rendered on the client by our JavaScript pack.
The reason for this is found in our test here:
RSpec.describe "HelloReact", type: :system do before do driven_by(:rack_test) end
By default, when we generate an RSpec system test, the test specifies that it should be driven_by(:rack_test)
. Rack::Test
is a testing API that allows you to simulate using a browser. It’s extremely fast, and that’s why it’s the default for RSpec system tests.
The downside of Rack::Test
is that because it doesn’t use a real browser, it doesn’t execute JavaScript code. So when we want our tests to exercise Webpacker packs, we need to use a different driver. Luckily this is as easy as removing the before
block:
RSpec.describe "HelloReact", type: :system do - before do - driven_by(:rack_test) - end - it 'should render a React component' do
Rails’ system test functionality uses selenium-webdriver
by default, which connects to real browsers such as Google Chrome. When we don’t specify the driver in our test, selenium-webdriver
is used instead.
Run the test again. You should see Google Chrome popping up and automatically navigating to your app. Our test passes! We’re relying on Chrome to execute our JavaScript, so we should get maximum realism in terms of ensuring our JavaScript code is browser-compatible.
One more useful option for a driver is “headless Chrome.” This runs Chrome in the background so a browser window won’t pop up. This is a bit less distracting and can run more reliably on CI servers. To run headless chrome, add the #driven_by
call back in with a new driver:
RSpec.describe "HelloReact", type: :system do + before do + driven_by(:selenium_chrome_headless) + end + it 'should render a React component' do
When you rerun the test, you’ll see a Chrome instance launch, but you should not see a browser window appear.
System tests are Rails’ built-in mechanism for end-to-end testing. An alternative end-to-end testing tool you may want to consider is Cypress. It’s framework agnostic and built from the ground up for rich frontend applications. One of the main benefits of Cypress is a GUI that shows your executing tests. It allows you to step back in time to see exactly what was happening at each interaction, even using Chrome Developer Tools to inspect the state of your frontend app in the browser.
But Rails’ system tests have a few benefits over Cypress as well. For experienced Rails developers, it’s helpful to write your system tests with the familiar RSpec and Capybara API and running them as part of the same test suite as your other tests. You can also directly access your Rails models to create test data in the test itself. In the past, doing so required something like the database_cleaner
gem because the server was running in a separate process, but Rails system tests handle wrapping both the test and server in the same database transaction. Because Cypress doesn’t have knowledge of Rails, setting up that data in Cypress takes some custom setup.
Whether you go with Rails system tests or Cypress, you’ll have the tooling you need to apply your testing skills to rich JavaScript applications.
Writing documentation is fun—really, really fun. I know some engineers may disagree with me, but as a technical writer, creating quality documentation that will...
Humanity has come a long way in its technological journey. We have reached the cusp of an age in which the concepts we have...
Go 1.18 has finally landed, and with it comes its own flavor of generics. In a previous post, we went over the accepted proposal and dove...