How to eliminate duplicated code in Rspec and Capybara specs
Duplicated code decreases readability and is hard to manage since eventual changes or fixes have to be propagated to all duplicated parts. Solution to such situation is to keep your code DRY meaning ‘Don’t repeat yourself’. This is one of the most basic programming principles that we should also use while writing specs for our applications.
Below I will show you some examples on how to DRY out your code and get rid of duplicates.
Put repeating lines to before block
code to improve
context 'random play' do
scenario 'plays random song with specific genre' do
create :artist_with_picture
app.home.load
app.home.player.random_settings.click
app.home.wait_for_random_play
app.home.random_play.select_genre_by_name 'Rock'
expect(app.home.artist_modal).to have_content 'Rock'
end
scenario 'plays random song with specific country' do
create :artist_with_picture
app.home.load
app.home.player.random_settings.click
app.home.wait_for_random_play
app.home.random_play.select_country_by_name 'Poland'
expect(app.home.artist_modal).to have_content 'Poland'
end
end
In these two specs we can see that first four lines are duplicated. This should give us a signal that we should make this code DRY.
Dry code
context 'random play' do
before do
create :artist_with_picture
app.home.load
app.home.player.random_settings.click
app.home.wait_for_random_play
end
scenario 'plays random song with specific genre' do
app.home.random_play.select_genre_by_name 'Rock'
expect(app.home.artist_modal).to have_content 'Rock'
end
scenario 'plays random song with specific country' do
app.home.random_play.select_country_by_name 'Poland'
expect(app.home.artist_modal).to have_content 'Poland'
end
end
We can move these four lines to the before block. Thanks to this, commands from the block will be executed at the beginning, before each scenario thus making our test DRY and readable.
Put repeating lines to methods
Code to improve
context 'when Admin is working on his skills' do
before { skills_page.load }
scenario 'he can add new skill' do
skills_page.add_new_skill.click
skills_page.skill_name.set 'capybara'
skills_page.skill_description.set 'test test test'
skills_page.skill_rate_type.select 'range'
skills_page.skill_category.select 'backend'
skills_page.requester_explanation.set 'test test test'
skills_page.create_skill.click
expect(draft_skills_page).to have_content 'New skill'
end
scenario 'he can edit skill' do
skills_page.edit_skill.first.click
skills_page.skill_name.set 'capybara'
skills_page.skill_description.set 'test test test'
skills_page.skill_rate_type.select 'range'
skills_page.skill_category.select 'backend'
skills_page.requester_explanation.set 'test test test'
skills_page.create_skill.click
expect(draft_skills_page).to have_content 'Edited skill'
end
end
In this situation we can see the same lines in the middle of the specs. Because first few lines of the specs differ from each other, we can’t use the before block.
Dry code
context 'when Admin is working on his skills' do
before { skills_page.load }
def skill_form(page)
page.skill_name.set 'capybara'
page.skill_description.set 'test test test'
page.skill_rate_type.select 'range'
page.skill_category.select 'backend'
page.requester_explanation.set 'test test test'
page.create_skill.click
end
scenario 'he can add new skill' do
skills_page.add_new_skill.click
skill_form(skills_new_page)
expect(draft_skills_page).to have_content 'New skill'
end
scenario 'he can edit skill' do
skills_page.edit_skill.first.click
skill_form(skill_edit_page)
expect(draft_skills_page).to have_content 'Edited skill'
end
end
We create a method with all of the repeating code inside so later we can refer to this method in the test. In this case we also supply page object as a parameter to the method.
Expect to change from to
Code to improve
scenario 'checks number of teams' do
expect(Team.count).to eq 4
teams_page.new_team_name_input.set('teamX')
teams_page.save_team_button.click
expect(Team.count).to eq 5
end
In the spec we check how the number of elements on the page changes after a certain action. We see repeated lines differing only when it comes to the expected number.
Dry code
scenario 'checks number of teams' do
teams_page.new_team_name_input.set('teamX')
expect { teams_page.save_team_button.click } # 2nd
.to change { Team.count } # 1st, 3rd
.from(4)
.to(5)
end
To fix this duplication we can use method ‘expect to change from to’. In this case, we check the number of elements on the page, then we expect that clicking on a button on the page will change the number of elements from x to y. It is worth noticing that we put all of the code in blocks { } . Thanks to this, actions are not performed chronologically, but in the order visible on the screen - at first the change block is evaluated to check for the initial value, then the expect action is triggered and lastly the change block is evaluated again to see if the from and to values are as expected.
Expect to change by
Code to improve
scenario "User unlikes songs" do
tracks_before = playlist.tracks.count
app.home.playlist.tracks.first.unlike
tracks_after = playlist.tracks.count
expect(tracks_after).to eq(tracks_before - 1)
end
In this case, at the beginning, we save the number of elements on the page, we delete one element and then check the number of elements after the action. At the end, we expect that the number of elements on the page after the action is one less than before the action. This spec is not bad but we can improve it.
Dry code
scenario "User unlikes songs" do
app.home.open_playlist
expect { app.home.playlist.tracks.first.unlike }
.to change { playlist.tracks.count }
.by(-1)
end
We expect that deleting an element on the page, changes the number of elements by -1.
What is the difference between ‘expect to change from to’ and ‘expect to change by’?
In the first function we are interested in the initial number of elements on the page and in the second we are only interested if it changes by the given number (initial state doesn’t interest us).
Loops in scenarios - each
Code to improve
context 'when user is admin' do
scenario 'he sees all pages in the navbar' do
expect(page.menu).to have_content 'Schedule'
expect(page.menu).to have_content 'Users'
expect(page.menu).to have_content 'Other'
expect(page.menu).to have_content 'Project info'
end
end
This is a classic situation in which we check the display of elements on the page which differ only by name. Checking each element individually causes many repetitions.
Dry code
context 'when user is admin' do
scenario 'he sees all pages in the navbar' do
links = ['Schedule', 'Users', 'Other', 'Project info']
links.each do |link|
expect(page).to have_content link
end
end
end
We create an array containing the names of repeating elements. We call each loop on the array so that every element of the array becomes our argument. Within the loop we check if given element is present on the screen.
another way of initializing an array
context 'when user is admin' do
scenario 'he sees all pages in the navbar' do
[
'Schedule',
'Users',
'Other',
'Project info'
].each do |link|
expect(page).to have_content link
end
end
end
Creating an array using %w notation.
Loops in scenarios - times
Code to improve
scenario 'creates empty list on the band page' do
band_page.create_empty_list.next_button.click
wait_for_ajax
band_page.create_empty_list.next_button.click
wait_for_ajax
band_page.create_empty_list.next_button.click
wait_for_ajax
band_page.create_empty_list.next_button.click
wait_for_ajax
expect(page).to have_content 'Success!'
end
In this spec we want to test creation of an empty song list. To do so we are going through the song list form by clicking the 'Next' button on 4 subsequent form pages without interacting with the form itself. This causes a lot of repetition.
Dry code
scenario 'creates empty list on the band page' do
4.times do
band_page.create_empty_list.next_button.click
wait_for_ajax
end
expect(page).to have_content 'Success!'
end
Times loop allows us to easily perform the same action set amount of times. We are using the times loop instead of the each loop because we don’t have a collection that we want to operate on and our action doesn’t need to be parametrized - we just want to simply repeat a plain action, like clicking on the button, N number of times.
Loops around scenarios
Code to improve
scenario "logs in user with role admin" do
user = create(:admin)
log_in(user)
expect(user).to be_logged_in
end
scenario "logs in user with role student" do
user = create(:student)
log_in(user)
expect(user).to be_logged_in
end
We have two identical scenarios that differ only in the type of user.
Dry code
roles = %w(admin student)
roles.each do |role|
scenarios "logs in user with role #{role}" do
user = create(role)
log_in(user)
expect(user).to be_logged_in
end
end
We can also do loops around whole scenarios. In the array we set list of users which we refer to when creating a given user. On the title of scenario we used interpolation to be able to distinguish between the scenarios. Also notice that the user roles that we put into the array match the names of the factories that we are using - this is crucial to be able to create different users per scenario.
Loops in the page objects
Code to improve
class PageObject < SitePrism::PageObject
element :eyes, 'input[name="user[face_attributes][eyes]”]'
element :nose, 'input[name="user[face_attributes][nose]”]'
element :lips, 'input[name="user[face_attributes][lips]”]'
element :mustache, 'input[name="user[face_attributes][mustache]”]'
end
The problem in this spec is that the page object elements only differ by the name in the selector.
Dry code
class PageObject < SitePrism::PageObject
[
:eyes,
:nose,
:lips,
:mustache
].each do |attribute|
element attribute, "input[name=\"user[face_attributes][#{attribute}]\”]"
end
end
We can use the loop also during adding page objects. We create an array with repeating elements. Then we use each loop with an argument. Within the loop we define each element using the arguments from the array - each argument is inserted as an element name and also interpolated into the selector.
create_list
Code to improve
context 'when user is a leader' do
let(:lider) { create :user, :leader }
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
let(:user_3) { create(:user) }
let(:user_4) { create(:user) }
scenario 'he sees his team members' do
expect(page.team.members).to have_content user_1.full_name
expect(page.team.members).to have_content user_2.full_name
expect(page.team.members).to have_content user_3.full_name
expect(page.team.members).to have_content user_4.full_name
end
end
We create each user separately causing a lot of repetitions.
Dry code
context 'when user is a leader' do
let(:lider) { create :user, :leader }
let(:users) { create_list(:user, 4) }
scenario 'he sees his team members' do
users.each do |user|
expect(page.team.members).to have_content user.full_name
end
end
end
The create_list is a factory bot (previously known as factory girl) method that creates a specific amount of users and returns them as an array. Then again we use each loop to check if all users are properly displayed on the page.
Shared example
Code to improve
describe 'logging process WITH ARGUMENT' do
let(:admin) { create(:admin) }
let(:student) { create(:student) }
let(:moderator) { create(:moderator) }
context 'for admin' do
scenario 'logs admin user' do
login_page.email.set admin.email
login_page.password.set admin.password
login_page.login_button.click
expect(page).to have_content admin.name
end
end
context 'for student' do
scenario 'logs student' do
login_page.email.set student.email
login_page.password.set student.password
login_page.login_button.click
expect(page).to have_content student.name
end
end
context 'for moderator' do
scenario 'logs moderator' do
login_page.email.set moderator.email
login_page.password.set moderator.password
login_page.login_button.click
expect(page).to have_content moderator.name
end
end
end
We have several very similar tests, differing only in the type of user, email and password.
Dry code
shared_examples 'user log in' do
scenario 'properly logs in' do
login_page.email.set user.email
login_page.password.set user.password
login_page.login_button.click
expect(page).to have_user_name
end
end
describe 'logging process WITH ARGUMENT' do
context 'for admin' do
let(:user) { create(:admin) }
it_behaves_like 'user log in'
end
context 'for student' do
let(:user) { create(:student) }
it_behaves_like 'user log in'
end
context 'for moderator' do
let(:user) { create(:moderator) }
it_behaves_like 'user log in'
end
end
When you have multiple specs that describe similar behavior, it might be better to extract them to shared examples. We include shared examples in our specs using one of these functions:
- include_examples "name" - include the examples in the current context
- it_behaves_like "name" - include the examples in a nested context
- it_should_behave_like "name" - include the examples in a nested context
- matching metadata - include the examples in the current context
In our specs we create shared example with variable user and then in every spec we call shared example and supply a user as its argument. This way the user variable in shared example will be set to the user that we supplied to given shared example.
shared_examples 'user log in' do |user|
scenario 'properly logs in' do
login_page.email.set user.email
login_page.password.set user.password
login_page.login_button.click
expect(page).to have_user_name
end
end
describe 'logging process WITH ARGUMENT' do
let(:admin) { create(:admin) }
let(:student) { create(:student) }
let(:moderator) { create(:moderator) }
context 'for admin' do
it_behaves_like 'user log in', admin
end
context 'for student' do
it_behaves_like 'user log in', student
end
context 'for moderator' do
it_behaves_like 'user log in', moderator
end
end
We can also create users earlier through let and later indicate by argument which use we want to check.
Splat operator
Code to improve
describe 'Main page' do
context 'when there are several users on the page' do
let(:user1) do
create(
:user,
name: 'Woody',
surname: 'Allen',
age: '90',
address: {
city: 'New York',
country: 'USA',
postal_code: '78-278'
},
nationality: 'American',
)
end
let(:user2) do
create(
:user,
name: 'Johny',
surname: 'Bravo',
age: '90',
address: {
city: 'New York',
country: 'USA',
postal_code: '78-278'
},
nationality: 'American',
)
end
let(:user3) do
create(
:user,
name: 'Jessica',
surname: 'Alba',
age: '90',
address: {
city: 'New York',
country: 'USA',
postal_code: '78-278'
},
nationality: 'American',
)
end
end
We create several users with specific and long data stored in hashes. A part of user data is identical for every user.
Dry code
context 'when there are several users on the page' do
let(:common_params) do
{
age: '90',
address: {
city: 'New York',
country: 'USA',
postal_code: '78-278'
},
nationality: 'American',
}
end
let(:user1) { create(:user, name: 'Woody', surname: 'Allen', **common_params) }
let(:user2) { create(:user, name: 'Johny', surname: 'Bravo', **common_params) }
let(:user3) { create(:user, name: 'Jessica', surname: 'Alba', **common_params) }
end
end
In our spec we create a let with the repeating data stored in hash. Then we create each user separately and use our common data with a double splat operator. Thanks to the double splat operator our repeated data will be ‘unpacked’ and supplied to each method just like the rest of the parameters.
The above examples are just suggestions for improving the code. Using these techniques won’t always increase the readability of the code, you have to do it with caution. But I think that in most cases they will reduce the amount of code and make it easier to maintain.