Introduction of Cypress on Rails

26 December 2019Dotby Anton Abramov

Cypress provides really powerful tools to create true end-to-end tests for modern web applications. With all these features we will stay 100% confident, that all frontend user interactions, even async requests, work as expected.

Another good thing, we can see all tests running live, including debugging and DOM inspecting, since Cypress uses Chrome as a test environment. Once all tests are done, it will store videos and screenshots of the testing process for you (if it’s needed). Cypress is totally ready to be integrated with any CI tool out of box.

Cypress with Rails

Let’s try to setup Cypress for existing Ruby on Rails project. Good news — we can use Cypress on Rails gem, which provides some good integrations between Cypress and RoR.

Installation is really straightforward and described here. All we need to do —  update Gemfile, install gem itself and generate boilerplate. With all this installed, we have the Cypress directories structure, which is ready to go.

Setting up the database

In general, we want to keep tests isolated as much as possible, so the result of each test stay repeatable no matter how many tests we run, in which order and so on. This paradigm forces us to provide some way to control data state. In particular, we want to set an initial state of data each time before the test.

Cypress provides us with hooks mechanism, which could solve this type of issues. In this case, we’re good to use only one of them - beforeEach. It will fire before each test in each test file and it’s good enough to solve the described problem. To check out all available hooks, please, take a look here.

// test/cypress/integration/for_non_registered.js
beforeEach(() => {
  cy.app("clean")
  // some logic to setup DB...
})

Seeding Database

Let’s talk about seeding itself. We have plenty of options on how we can setup data in DB

Using regular seed scenarios

In this case, we could use the power of scenarios. Actually, the scenario is a simple rb file, which Cypress on Rails executes for us. Inside this file, we could put any logic, for example, regular seed.rb content.

# test/cypress/app_commands/scenarios/everything_active.rb
# frozen_string_literal: true

user1 = User.create!(
  email: 'user1@test.com',
  password: 'user1_password'
)

user2 = User.create!(
  email: 'user2@test.com',
  password: 'user2_password'
)

Todo.create!(
  title: '1 hour walk',
  completed: false,
  user: user1
)

Todo.create!(
  title: 'Go shopping',
  completed: false,
  user: user1
)

Todo.create!(
  title: 'Buy some food',
  completed: false,
  user: user1
)

Todo.create!(
  title: 'Wash my car',
  completed: false,
  user: user2
)

Todo.create!(
  title: 'Go to gym',
  completed: false,
  user: user2
)

After that we can use this scenario in our test suite

// test/cypress/integration/for_non_registered.js
...
beforeEach(() => {
  cy.app('clean')

  // using scenarios
  cy.appScenario('everything_active')
})
...

Using Rails test fixtures

This one depends on the Rails project, which might use fixtures or not. If your project uses minitest, rather than rspec, then you likely have fixtures. By default, these .yml files can’t be used directly. Luckily, we can use predefined command for this:

# test/fixtures/users.yml
user1:
  email: "user1@test.com"
  encrypted_password: <%= BCrypt::Password.create('user1_password', cost: BCrypt::Engine::MIN_COST) %>

user2:
  email: "user2@test.com"
  encrypted_password: <%= BCrypt::Password.create('user2_password', cost: BCrypt::Engine::MIN_COST) %>

After that we can load fixtures in our test suite

// test/cypress/integration/for_non_registered.js
...
beforeEach(() => {
  cy.app('clean')

  // loading Rails fixtures
  cy.appFixtures()
})
...

Using FactoryBot

The third option is to load data using FactoryBot. In this case, we have an option to create entries one by one separately or execute a bulk operation:

# test/factories/users.rb
# frozen_string_literal: true

FactoryBot.define do
  factory :user do
    email { Faker::Internet.email }
    password { 'test_password' }

    factory :user_with_todos do
      transient do
        todos_count { Faker::Number.between(from: 1, to: 5) }
      end

      after(:create) do |user, evaluator|
        create_list(:todo, evaluator.todos_count, user: user)
      end
    end
  end
end

# test/factories/todos.rb
# frozen_string_literal: true

FactoryBot.define do
  factory :todo do
    title { Faker::Lorem.sentence(word_count: 3) }
    completed { Faker::Boolean.boolean }
    user
  end
end

After that we can load data using fixtures in our test suite

// test/cypress/integration/for_non_registered.js
...
beforeEach(() => {
  cy.app('clean')

  // using factory bot
  cy.appFactories([
    ['create_list', 'user_with_todos', 3]
  ])
})
...

Logging in

Now, when DB is populated with some data, let’s test application. In some cases it’s not a big deal, all we need is writing proper instructions for the desired page in the test file. But sometimes we need a little bit more. For example, we need to test some features under a restricted area, such as a profile page.

So, we need a good solution on how to log in before writing the test itself. There are different ways to do it:

  • We could log in manually, during the test. This means we need just fill login form and submit it with Cypress. It’s really simple, but we don’t want to do it every time. It will cause a performance issue and our test suites will run too slow.
  • We could create some sort of POST request using cy.request() call, to simulate real form submission. At this point, all depend on backend codebase, because we need to handle submitted data and return a proper response (like a token or something else).

Let’s take a look at the 2nd option closely. Basically, we need to define custom command to put login logic in there. So, here is a possible solution to achieve that:

  • Add login endpoint to Cypress on Rails config file cypress.json. It’s very useful to keep some common params for Cypress. You can find the docs for the cypress.json here.
// test/cypress.json
{
  "baseUrl": "http://localhost:5001",
  "defaultCommandTimeout": 10000,
  "loginEndpoint": "/sign_in_before_test"
}
  • Define the new route in the Rails app. Make sure that this route available only for test environment
# config/routes.rb
# for Devise gem case
...
devise_scope :user do
  get 'sign_in_before_test' => 'users/sessions#sign_in_before_test' if Rails.env.test?
end
...
  • Put custom command for login action into command.js file
// test/cypress/support/commands.js
Cypress.Commands.add("login", email => {
  cy.request({
    url: Cypress.config().loginEndpoint,
    qs: { email },
  })
})
  • After that, we could use login command wherever need to
// test/cypress/integration/todo_management.js
const userEmail = "user1@test.com"

...

beforeEach(() => {
  // setup DB and other stuff...

  cy.login(userEmail)
})
...

The rest of things depends on the backend. /sign_in_before_test should work as instant backdoor for login action, but only for the test environment.

So, how could login action work actually?

Third party gems

The project could use third party gem for authentication feature. For example, it could be Devise (or Clearance) and this is the most common way to handle it. In this case, for example, we could build login action with Devise helper.

# app/controllers/sessions_controller.rb
# for Devise gem case

...
def sign_in_before_test
  user = User.find_by(email: params[:email])
  if user.present?
    sign_in(user)
    render json: { success: true }
  else
    render json: { shopId: false }
  end
end
...

The project could use some custom solution for authentication. In this case, it depends on the implementation, but overall the idea stays the same.


All described here could be found in the demo project. To test things out by yourself check this out https://github.com/shakacode/rails-react-redux-todomvc

Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right