Rails, Cypress, testing the whole stack is definitely easier
0. The origins : the Rails doctrine™
At BootrAils, Cypress is already integrated, configured, with a few tests to cover and document the application. We find this tool very interesting because it matches one precise point of the Rails philosophy :
Value integrated system
You can read the whole paragraph here.
Which means, less boundaries, blurred lines between View, Controller and Model, despite being a MVC framework. This probably hurts for any newcomer to Rails.
At the end of the day, you are more productive, even if layers are not perfectly separated.
Question : Given that unit testing requires to test things in isolation, how do you test anything when layers are precisely not isolated ?
Fortunately, some kind of testing are actually testing layers together :
- integration testing
- system testing
- end-to-end testing
All these kinds of testing are checking behavior from the outside.
1. Testing Rails application from top to bottom : not so easy
Before Cypress, testing the whole stack with Rails was not so satisfying. By “whole stack” we mean testing the whole running server, database, controllers, etc, through the UI, like a regular user will do. You had to glue multiple drivers, gem and libs together, and you ended with a not-so-well-stabilized testing screen suite. Selenium users know how complicated it is to achieve great work in this area.
2. Enter Cypress
Cypress has the particularity not to care about the underlying tested screen. It doesn’t matter if you use jQuery, Hotwire, AlpineJS or React in the front-end.
Cypress brings a lot of positiveness amongst developers, at BootrAils we particularly love it. Others too :
Cypress replaces the need for Capybara.
3. Cypress and Rails, a incredible wedding
Now a very good news : integrating Cypress to Rails is really simple, because there’s already a gem for that. Thanks to the amazing work of the teamdouble team. Their corresponding GitHub repository is here.
4. Tutorial, from scratch
Let’s install a fresh new Rails application, and test this beautiful gem.
First, we are checking our seatbelts :
1
2
3
4
5
6
7
8
9
10
$> ruby -v
ruby 3.0.0p0 // you need at least version 3 here
$> bundle -v
Bundler version 2.2.11
$> npm -v
8.3.0 // you need at least version 7.1 here
$> yarn -v
1.22.10
$> psql --version
psql (PostgreSQL) 13.1 // let's use a production-ready database locally
Then, we create a bare Rails application :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
mkdir rails-with-cypress && cd rails-with-cypress
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '7.0.0'" >> Gemfile
bundle install
bundle exec rails new . --force --css=bootstrap -d=postgresql
bundle update
# Create a default controller
echo "class HomeController < ApplicationController" > app/controllers/home_controller.rb
echo "end" >> app/controllers/home_controller.rb
# Create another controller (the one that should not be reached without proper authentication)
echo "class OtherController < ApplicationController" > app/controllers/other_controller.rb
echo "end" >> app/controllers/other_controller.rb
# Create routes
echo "Rails.application.routes.draw do" > config/routes.rb
echo ' get "home/index"' >> config/routes.rb
echo ' get "other/index"' >> config/routes.rb
echo ' root to: "home#index"' >> config/routes.rb
echo 'end' >> config/routes.rb
# Create a default view
mkdir app/views/home
echo '<h1>This is home</h1>' > app/views/home/index.html.erb
echo '<div><%= link_to "go to other page", other_index_path %></div>' >> app/views/home/index.html.erb
# Create another view (will be also protected by authentication)
mkdir app/views/other
echo '<h1>This is another page</h1>' > app/views/other/index.html.erb
echo '<div><%= link_to "go to home page", root_path %></div>' >> app/views/other/index.html.erb
# Create database and schema.rb
bin/rails db:create
bin/rails db:migrate
Good ! A quick check that everything is running properly, at least locally, by launching :
1
./bin/dev
And open http://localhost:3000
Navigate from one page to another. This is the main feature of our app (wow!). And we don’t want to test it manually each time we push a new feature in production, so let’s automate its testing properly.
5. Install Cypress gem for Rails
First you will need Cypress itself. Run :
1
yarn add --dev cypress
Add this to your Gemfile :
1
2
3
group :development, :test do
gem "cypress-rails"
end
And run :
1
2
3
4
5
6
7
8
9
10
bundle install
# verbose logs...
# ...
# ...
Using rails 7.0.0
Fetching cypress-rails 0.5.3
Using turbo-rails 7.1.1
Installing cypress-rails 0.5.3
Bundle complete! 18 Gemfile dependencies, 69 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Great ! Cypress is installed.
Now run :
1
bin/rails cypress:init
Ok ! only the cypress.json file has been created
1
2
3
4
5
{
"screenshotsFolder": "tmp/cypress_screenshots",
"videosFolder": "tmp/cypress_videos",
"trashAssetsBeforeRuns": false
}
Above are the default options of cypress-rails, you can find all available options here
6. Creating files
As you may have notice, you have neither test or directory structure so far. Cypress is smart enough to provide everything needed the first time you run it. Run
1
bin/rails cypress:open
Wait some seconds, then the IDE of Cypress should appear, like this :
Cypress suggests you keep or remove existing tests. We suggest you keep them, they are super-useful as a reference.
Once kept, cut/paste them elsewhere (in another place of your workspace), so that they won’t make too much noise in this tutorial (and in your own project, generally speaking).
Here is the created directory structure, at the root of your Rails project :
1
2
3
4
5
6
7
8
9
10
root
+-- cypress
+--fixtures
+--example.json
+--integration
+--plugins
+--index.js
+--support
+--commands.js
+--index.js
the integration
folder is where you actually write your test : all files ending with *.spec.js
will run in the Cypress IDE.
The fixtures
folder is where you define common reference data for your tests, support
and plugins
folders are here to allow you to extend Cypress functionalities.
7. Our own test
As you may have noticed, the integration folder is not empty. Copy/paste all the content somewhere else - examples inside are really useful, so keep them as reference for later. Then empty completely the content of the integration
folder.
And write the following test :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// inside cypress/integration/home.spec.js
describe('Testing Home page', () => {
beforeEach(() => {
cy.visit('/')
})
it('Display a title by default', () => {
cy.get('h1').should('contain', 'This is home')
})
it('Allows to navigate from home page, to another', () => {
cy.get('a[href*="other/index"]').click()
cy.location().should((location) => {
expect(location.pathname).to.eq('/other/index')
})
})
})
Then in your terminal run :
1
bin/rails cypress:open
And click on home.spec.js
in the IDE
Great ! Cypress offers a nice IDE that offers nice debugging options.
Back in your terminal, stop Cypress (Ctrl+C) and run
1
bin/rails cypress:run
Amongst other verbose logs, you should see :
1
2
3
4
Testing Home page
✓ Display a title by default (542ms)
✓ Allows to navigate from home page, to another (210ms)
2 passing (776ms)
Good ! Our Cypress suite can run on CI, there is actually no need for an IDE for each scenario.
8. A word of caution
It was a gentle introduction to Cypress and Rails. From here, we suggest you to read carefully the README of https://github.com/testdouble/cypress-rails, particularly the way to initiate the Rails database.
Here at BootrAils we found it extremely useful for 2 cases :
- The “happy path” of your application : you don’t want the main functionality of your app to break on each release. The healthiest way to test is when everything is glued together from A to Z.
- The corner cases where JavaScript and Rails controllers need to work tightly together. The only way to test it properly is with this kind of testing.
However, don’t try to reach every use case with Cypress. Context is harder to reach with this kind of testing; moreover, these tests are slower than plain Ruby-based unit tests.
Apart from these warnings, we found that end-to-end testing is finally a breeze of fresh air, thanks to Cypress.
Enjoy !