How to eliminate duplicated code in Rspec and Capybara specs

Photo of Dorota Niewęgłowska

Dorota Niewęgłowska

Updated Jan 19, 2023 • 18 min read
annie-spratt-522097

When our test suite grows we start to notice that some parts of our code get duplicated.

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.

Photo of Dorota Niewęgłowska

More posts by this author

Dorota Niewęgłowska

Dorota is an ardent opponent of the existence of bugs, regardless of its origin. She fights for...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business