Post

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.

Unfortunately “the simplest possible Rails + RSpec + Capybara test” is still not particularly simple

—Jason Swett, “hello world” using RSpec and Capybara

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 :

We switched to cypress at work from capybara, and I will never be going back.

—found on Reddit.

It’s not that Capybara is dead, but the Cypress experience is just leaps better..

—found on Reddit 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 1st launch
cypress 1st launch

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

cypress demo
cypress demo

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 !

This post is licensed under CC BY 4.0 by the author.