Cypress for Ruby on Rails developers

Snehacynixit
7 min readAug 29, 2020

The web has evolved and so did Ruby on Rails. The framework now provides improved JavaScript tooling, including integrations for both yarn package manager and webpack module bundler — de facto choice in the JavaScript community. Out of the box support for these tools facilitates creation of rich client applications. We can now easier than ever build Rails-backed applications with modern tools like Vue, React and Angular.

However, as we develop our applications with modern JavaScript libraries, a new concern emerges. Feature specs that once brought confidence are now becoming increasingly brittle. Specs that once steadily navigated through almost static web pages now stumble and fall in an asynchronous world of JavaScript. ruby on rails training helps you to learn more skills and techniques from industrial experts.

Hence, similarly to how webpack and yarn were previously introduced to build modern applications, we further introduce Cypress to test them. Particularly, in this post, we look at Cypress as an alternative tool for writing and executing feature specs. We explore the key points that make it stand out among other web automation tools. Finally, we demonstrate how to integrate Cypress into a Ruby on Rails development cycle by setting up tests in both local and CI environments.

Cypress

Cypress is a JavaScript end-to-end testing framework. It is an all-in-one solution that enables one to both write and execute tests that run in the browser. In a Rails project, Cypress takes place of both Capybara and the underlying web driver.

Cypress vs Selenium

Without doubt, Capybara is the most popular and widely adopted solution for integration testing in Rails. It provides a robust interface to communicate with the browser through a web driver. Although Capybara is agnostic of the installed driver, Selenium WebDriver is the prevailing choice among Rails developers. As Selenium is the most widespread solution also beyond Rails, it is therefore important to understand the difference between Cypress and the combination of Capybara/Selenium.

The key difference between Cypress and Selenium lies in their ways of communication with the browser, i.e. browser automation. With Selenium, all communications are done through an external to the browser driver (the WebDriver). To simulate user actions, Selenium queries the browser by sending remote calls through the network and then extracting the desired information from the responses. Cypress, on the other hand, doesn’t involve any network communication. It lives directly in the browser and runs the tests in the same run-loop as the client application. That is, Cypress tests are right there — in the browser, sharing the same context with your application.

As an outside process, Selenium is not able to react to events in the same run-loop. Events are thus processed asynchronously, can be overlooked and network delays may be introduced. This may lead to test flakiness and longer execution times.

In contrast, by sharing the same JavaScript event-loop with your application, Cypress is synchronously notified of all events as they occur leading to more reliable test execution. Removing the network layer allows Cypress to execute tests as fast as the browser is capable of rendering.

The browser-integrated architecture of Cypress accounts for it’s quick and reliable test execution. Sounds promising. But let’s see it for ourselves. Next move: install Cypress in a Rails project. ruby on rails online course from industrial experts.

Cypress on Rails: hands-on experience

Application setup

We want to quickly bootstrap a Rails application with some logic, data, and views already in place so that we can focus on the testing part. A fresh installation of Solidus will do the job. Solidus is a highly customizable ecommerce platform. By adding Solidus gem to our project we get a default store implementation right out of the box. Bellow is an outline of all required application setup steps:

  1. Create a new Rails project, use Postgresql for the database and skip the test files:
$ rails new cypress-store -d postgresql -T
  1. Add Solidus to the project by installing two new gems and running their included generators:
# Gemfilegem 'solidus'gem 'solidus_auth_devise'bundle exec rails g spree:installbundle exec rails g solidus:auth:installbundle exec rake railties:install:migrations
  1. Run the rails server and navigate to the index page, you should be greeted with the default store landing page:
bundle exec rails s

At this step, we have a full-fledged online store. One might want to tweak some styles before sending it to production, but for our testing purposes, it’s just what we need. We are now ready to setup Cypress.

Cypress setup

Similarly to how react-rails gem is used to integrate React components with Rails views, we will use cypress-on-rails gem to integrate Cypress with Rails tests. The new gem is a light wrapper around Cypress that augments its functionality allowing us to prepare application state using factories or custom Ruby code. Add the cypress-on-rails gem to the Gemfile and run the generator:

# Gemfilegroup :test, :development do  gem 'cypress-on-rails', '~> 1.0'endbin/rails g cypress_on_rails:install

This will both add the cypress npm package and setup the Ruby integration. To ensure that Cypress is ready to run tests against our application, run the application server in a test environment on port 5002 and in a separate window run Cypress:

# start railsbundle exec rails server -e test -p 5002# in separate window start cypressyarn cypress open --project ./spec

You might also need to precompile assets before running Cypress. The command cypress open will launch a test runner window with some sample tests scaffolded for us by Cypress. We will move on to substitute the sample tests with our own. Now, notice how simple it is to add Cypress to a Rails project, a single gem and no external dependencies like servers or drivers required.

However, let’s throw in a few more gems to help us with arranging the application state. We want to empty the database before each spec and then create the data required by each test scenario.

As usually, we delegate the database cleaning job to the database cleaner gem. Add the gem to the project and then key in the bellow scripts:

# spec/cypress/cypress_helper.rbrequire 'database_cleaner'# spec/cypress/app_commands/clean.rbDatabaseCleaner.strategy = :truncationDatabaseCleaner.clean# spec/cypress/support/index.jsimport './commands'import './on-rails'beforeEach(() => {  cy.app('clean');});

The above scripts configure the gem and then run it (cy.app('clean')) before each integration test. Now lets prepare the test data with FactoryBot. Add the factory_bot_rails gem to the project together with the following code:

# spec/cypress/cypress_helper.rbrequire 'database_cleaner'require "factory_bot"require "spree/testing_support/factories"require "carmen"Dir[Rails.root.join("spec", "support", "factories", "**", "*.rb")].each { |f| require f }FactoryBot.find_definitions

We now have access to both the FactoryBot library and all the factories provided by Solidus (e.g. users, orders and products).

Add Cypress tests

Some online stores require their customers to sign in prior to the checkout. Solidus comes with user session management functionality built-in and for us it’s a great test subject to begin with.

Our first test will verify that only registered (persisted) users can sign in. Users that cannot be found in our database will be presented with the corresponding sign in failure message.

Following the Arrange-Act-Assert (AAA) testing pattern, we first arrange the application state for our planned scenario. We will execute our tests against an application with one existing user. We will use a user factory imported from Solidus. Key in the following:

# spec/cypress/app_commands/scenarios/sign_in.rbFactoryBot.create(:user, email: 'user@example.com', password: 'test123')

With the state arranged, we move on to describe user actions and assert the desired outcome. Note that Cypress leverages other well-known libraries to write and organize tests. Specifically, Mocha provides TDD-like syntax to structure the tests (think decribe() — beforeEach() — it()). Likewise, Cypress wraps and extends Chai for writing assertions (think should). Moreover, with cypress-on-rails gem we are able to run our factory defined above by calling cy.scenario(‘sign_in’). Type in the following:

// spec/cypress/integration/user/sign_in.spec.jsdescribe('Sign in', () => {  beforeEach(() => {      cy.scenario('sign_in');  });});

Great, with above we declared that we want our sign_in state preparation script to run before any of the following tests. Now, let’s describe a scenario in which an existing user attempts to login:

// spec/cypress/integration/user/sign_in.spec.jsdescribe('Sign in', () => {  beforeEach(() => {    cy.scenario('sign_in');    cy.visit('/');  });  it('signs in user with valid credentials', () => {    cy.get('#link-to-login').click();    cy.url().should('include', '/login');    cy.get('[name="spree_user[email]"]').type('user@example.com');    cy.get('[name="spree_user[password]"]').type('test123');    cy.get('input.button')      .contains('Login')      .click();    cy.get('.flash.success')      .contains('Logged in')      .should('be.visible');  });});

The test code is self-explanatory. The user visits the index page and clicks on the login button, which redirects him to the login page. On the login page, the user enters his valid credentials and clicks “Login” button. As the credentials are valid, we expect a login success message to be displayed. Now let’s verify that a user with invalid credentials cannot sign in:

// spec/cypress/integration/user/sign_in.spec.jsdescribe('Sign in', () => {  beforeEach(() => {    cy.scenario('sign_in');    cy.visit('/');  });  it('signs in user with valid credentials', () => {    ...  });  it('does not sign in user with invalid credentials', () => {    cy.get('#link-to-login').click();    cy.url().should('include', '/login');    cy.get('[name="spree_user[email]"]').type('wrong_email@example.com');    cy.get('[name="spree_user[password]"]').type('test123');    cy.get('input.button')      .contains('Login')      .click();    cy.get('.flash.error')      .contains('Invalid email or password.')      .should('be.visible');  });});

It’s not ideal, the duplicate code is screaming at us. We will return to it, but for now, we focus on executing the tests. ruby on rails online training along with real time projects.

--

--