Introduction to Cypress on Rails
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: '[email protected]',
password: 'user1_password'
)
user2 = User.create!(
email: '[email protected]',
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: "[email protected]"
encrypted_password: <%= BCrypt::Password.create('user1_password', cost: BCrypt::Engine::MIN_COST) %>
user2:
email: "[email protected]"
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 thecypress.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 = "[email protected]"
...
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