Traditionally, Rails gives us a full stack development framework including E2E tests with Selenium to develop websites. Let's see how to transition an app using Rails' built in system tests to using Cypress, a new E2E framework built on Node.js, targetting modern JavaScript heavy applications.
A common Rails stack looks like:
- RSpec for the testing framework
- FactoryBot for populating the database
- DatabaseCleaner (or just ActiveRecord) for cleaning the database between tests
- Selenium for driving the browser in E2E tests
Moving to Cypress (at least for the E2E tests), it now looks like:
- Mocha/Chai combo for the testing framework
- No good replacement for FactoryBot
- Need to figure the database clearing/truncation out on our own
- Cypress for the browser tests
At first glance, and based on my experience, the stack is a lot less "batteries included", which is what I like about Rails. I'm continuing to try new things out. This article will
- Set up the traditional stack, and make a simple CRUD app with a few simple E2E tests
- Move to the cypress.io stack, while implementing the same tests
- Dicuss improvements and thoughts
I like each blog post to be independant, and include all the steps to recreate it. If you don't care about setting up the Rails app with RSpec etc, just grab the repo here and move to the second half.
Note: If you want to skip to the section where I add Cypress, ctrl+f "Installing and Setting Up Cypress".
Generate the Rails app, skipping MiniTest and using Postgres for the database with rails new cypress_app -T --database=postgresql
. Update group :development, :test
in the Gemfile
:
Add FactoryBot and RSpec and webpacker.
https://gist.github.com/61a9f1a74181246c10b08fe23e234c35
Then run bundle install
, and generate the binstub and system
folder by running:
https://gist.github.com/d93603b557e962332a16f1717e6bde5d
Initalize the database with rails db:create
. That should have set up RSpec, FactoryBot and installed the dependencies for system tests.
We will make a simple blog app, that lets an anonymous user create a post, which has a title
, body
and category
. We need a Post
and Category
model - create them with the following:
https://gist.github.com/e8146c71b1c6301587f812a9d0dfe2e0
Next, we need a posts_controller
to create posts. Create one with touch app/controllers/posts_controller.rb
. We will come back to this in a moment.
Update models/category.rb
to reflect the has_many
relationship (a category can have many posts):
https://gist.github.com/50a67657ad4d504a68b6011d2ec9524b
Update config/routes.rb
:
https://gist.github.com/1aa5f729579bd3fb5bb76001de698d9b
Add some code to app/controllers/posts_controller.rb
:
https://gist.github.com/75ae99489a916e39bf2cd7642c4b636e
Create some views with
https://gist.github.com/15751d01e8878ac89d597b1e9a0f2058
Create a test with touch spec/system/posts_spec.rb
, and add:
https://gist.github.com/8989ab40a2f48aecdfa3bc1a16d27075
Make sure everything is working by running rspec spec/system
. If the test passes, everything is working correctly.
Before moving on to using Cypress, let's make sure the code is working correctly using the built in system tests, which run using selenium_chrome_headless
. Update spec/system/posts_spec.rb
:
https://gist.github.com/ffbfb83863cbfdd909fd3161d743a3a3
This fails with:
https://gist.github.com/4bf1bf06270ab8383388c4f7a5e70e7d
Update app/controllers/posts_controller.rb
first:
https://gist.github.com/52d94c38ce7a4eacc189e9fbebf42bca
Now we need the views. Start with app/views/posts/_form.html.erb
:
https://gist.github.com/027703a2b67eeb3879c3bde1bace764d
We included a flash message validating the minimum length of a post - we will add this validation in a moment. First, update app/views/posts/new.html.erb
:
https://gist.github.com/81c9eff23e5446f184c9b9909846e73a
And lastly, app/views/posts/show.html.erb
:
https://gist.github.com/3e329f4a40edb15224eb4bcc6e40be77
Now running rspec spec/system
should give us a passing test. Let's implement two more tests, starting with validating the length of a post title. Update app/models/post.rb
.
https://gist.github.com/19559b07a8b51c8f1041f4ef0afe9eda
Next, update spec/system/posts_spec.rb
:
https://gist.github.com/ff00fb8c4f6a970d3a23229eb3dd4da2
This should pass, too.
Finally, add the following to app/views/posts/index.html.erb
:
https://gist.github.com/058a4ad3e9d02211d298b4ae142ca7ed
This shows a list of posts at /posts
. Lastly, a test in spec/system/posts_spec.rb
:
https://gist.github.com/84553230f9c78ebb91bc689a7714363f
Running rspec spec/system
should yield three passing tests.
Now we have a boring, yet working and well tested Rails app. Let's proceed to add Cypress and migrate our test suite. Firstly, install Cypress and a few dependecies with:
https://gist.github.com/64c6a9cd81fec3c0cc78c8345d8c2dd7
Next, following their documentation, add a command to package.json
. Mine package.json
looks like this:
https://gist.github.com/c711d5f9fc0e2e70742094efe64fc6cc
Finally, run yarn cypress:open
. You should see:
Furthermore, a cypress
folder was created for you.
Let's migrate the first test - creating a post succesfully - to Cypress. First, start the rails server by running rails server
in a separate terminal from Cypress. Next, create the test with touch cypress/integration/posts.spec.js
, and add the following:
https://gist.github.com/197dd1feda55495078c63981f15eb0f1
The Cypress DSL is fairly easy to read. Strictly speaking, {force: true}
should not be necessary. Some of my tests were randomly failing without this, though, so I added it. I'll investigate this in more detail later.
If you still have the Cypress UI open, search for the test using the search box:
This fails, of course:
Because no categories exist. Before implementing a nice work around, just create one by dropping down into rails console
and running Category.create!(name: 'ruby')
. Now the test passes!
There are some problems:
- Running the tests in the development env is not good. We should use
RAILS_ENV=test
. - Need a way to seed some data, like a category.
- Should clean the database between each test.
Let's get to work on the first two.
Let's set up some basic seed data for the tests to use. First, create a seeds
folder containing a test.rb
file by running mkdir db/seeds && touch db/seeds/test.rb
. Inside, add:
https://gist.github.com/a910402ad082433244eee58216f7d269
Next, in db/seeds.rb
add:
https://gist.github.com/eb38b069333841f66321792ab2c6f813
This will seed the correct seed file based on the current RAILS_ENV
.
Now we have a way to seed data, but no way to clean the database after each test. The way I've been handling this is by making a POST request to dedicated /test//clean_database
endpoint before each test, as recommended by Cypress. Let's make that API. First, update config/routes.rb
:
https://gist.github.com/a023d5db3459b0d517e59f911c4d0224
Next create the controller and spec: mkdir app/controllers/test && touch app/controllers/test/databases_controller.rb
and mkdir spec/controllers && mkdir spec/controllers/test && touch spec/controllers/test/databases_controller_spec.rb
.
Starting with databases_controller_spec.rb
, add the following:
https://gist.github.com/4067a8b2470f56e0bad96a055b1410c6
There are two functions this API provides. Both specs test for truncation. We also allow a should_seed
parameter to be provided. If should_seed
is true, then we repopulate the database using the data defined in db/seeds/test.rb
.
The controller implementation is as follows:
https://gist.github.com/6785c2ebe503c113f28f78a086e9ad59
This should yield two passing specs. Now, restart the Rails server with RAILS_ENV=test rails server
. Now, we need a way to actually access the API from within Cypress. Inside of cypress/support/commands.js
, add the following:
https://gist.github.com/55a98acd5ac0f9e391d732e3ea70eb31
Cypress automatically loads all the helpers in commands.js
for us.
Since Rails is running on port 3000, and Cypress is assigned an arbitrary port, we need to support CORS for the /test
routes. Inside config/environments/test.rb
, add the following:
https://gist.github.com/eb8f4e669ec9bf0c78d3b5b263d4d366
This allows CORS for the test environment only. Restart the Rails server, and reopen the Cypress UI if you closed it. It should pass... alas, it does not.
If you look closely, only on the initial opening of the Cypress UI, the browser kind of "flickers" once. For some reason, this causes the beforeEach
hook to be called twice, messing up the seed data. The post request contains the category id of the first seed run, however since the browser flickers and causes the data to be reseeded, the initial category id used in the test no longer exists!
Once you have the UI running, however, simply rerunning the test should be enough to pass. Typically I only open the UI once, and leave it open, so it is not a big deal locally. On CI, this is a huge problem though. I'm going to get in contact with the Cypress team and see if they have a work around.
One last thing I want to add is the ability to seed some data, depending on the test. For this, I'll use another test-env-only controller. Create it with touch app/controllers/test/seeds_controller.rb
. Add a test with touch spec/controllers/test/seeds_controller_spec.rb
. Add the following test:
https://gist.github.com/a4b87a375b194fc9941f190f3cf17a12
This endpoint will simply seed a specified number posts. Now, the implementation in seeds_controller.rb
:
https://gist.github.com/c5209e72548f203bd06ddee51a676ef6
This test should pass. Here are two more tests - one for the case where a post title is too short, and an error is displayed, and another for the /posts
index page. This once will make use of the new /seed_posts
route, so update commands.js
:
https://gist.github.com/aa825cb54ab576850d12dbed8722e03c
Everything passes!
This was a very long article. We covered:
- Setting up a traditional Rails app
- Installing Cypress
- Creating specific route for cleaning and seeding the database
- Using Cypress hooks, such as
beforeEach
, and custom commands
Cypress is certainly a great tool, and a refreshing new angle on E2E testing. The lack of support for non Chromium based browsers, and of information on how to integrate it with various backends led to some challenges. However, I'm positive Cypress is going in a good direction and will continue to refine my workflow and integration with Rails.