Selenium Hates Him

Learn his one weird trick for defeating integration test boilerplate!

Selenium Hates Him

Introduction

Using RSpec and Capybara to feature test Rails applications is one of the most useful and common combinations out there. The ability to interact with the front-end of your application as a user brings a great level of confidence that the features being added are working properly and playing nicely together.

As the complexity of those features increases there usually comes a time where the complexity of the Capybara actions also increases, sometimes at a greater rate than the features themselves. The RSpec tests get littered with very specific instructions on how to choose an option from a particular drop-down, fill in a complicated form, or navigate through several nested menus to find one particular option.

This creates a large amount of noise when reading through those specs and decreases the ability to quickly scan the code and see what is being done. Keeping these instructions in the specs also has a higher chance of duplicating simple interactions that have a way of being implemented slightly differently every time they are written.

This is where the Page Object Pattern can really help.

This pattern entails building a reusable Page class that the rest of your individual page objects can inherit from that provides access to the Capybara DSL and RSpec matchers.

I like to keep all of these files in the /spec/features/pages/ directory.

You will need to require these files in /spec/rails_helper.rb.

Make sure that the base page file is required first:

require Rails.root.join('spec/features/pages/page.rb')

And then the rest:

Dir[Rails.root.join('spec/features/pages/**/*.rb')].sort.each { |f| require f }

Building the base Page Object

The first step is to create a base Page class that includes a few handy modules:

class Page
  include RSpec::Matchers
  include Capybara::DSL
  extend Capybara::DSL
end

This base class is also a handy place to put in a few globally useful methods that will apply to all parts of the application. If these start to get unwieldy they can always be split out into smaller classes that group the methods by functionality.

One of the more useful methods below is the react_select_option. When using the React Select library choosing an option with Capybara is not as simple as using the built-in select_option. This method allows for selecting an option within a specific react-select drop-down by supplying a class_prefix and the option text.

def refresh
  page.visit(page.current_path)
end

def logo
  page.find('#main-logo')
end

def title
  page.find('span.page-title')
end

def has_error?
  page.find('.error-alert-message') != nil
end

def has_permissions_error?
  has_selector?('.alert.flash-alert p', text: 'You are not authorized to access this page.')
end

def alert
  page.find('div.flash-alert')
end

def alert_message
  page.find('div.alert-message')
end

def react_select_option(class_prefix, option)
  r_select = find(".#{class_prefix}")
  r_select.click
  expect(r_select).to have_css(".#{class_prefix}__menu")
  r_select.find(".#{class_prefix}__option", text: option).hover
  r_select.find(".#{class_prefix}__option", text: option).click
end

Page Components

Now that there is a base Page class to inherit from the page specific classes can be created. These apply to a certain page of the application and contain methods that will only apply to that page.

The main method here is the self.visit method. This will be used in the RSpec spec to initialize the class and allow the methods to be called.

If the page specific class needs to be initialized with any additional information that can be done as well.

Here is an example of building up a class that can be reused for signing in a user:

class SignIn < Page
  def self.visit
    page.visit '/users/sign_in'
    new
  end
end

These are specific methods that are only used on that particular page:

def sign_in(email, password)
  within('#sessions-new') do
    fill_email(email)
    fill_password(password)
    click_button 'Sign in'
  end
end

def fill_email(email)
  fill_in 'user_email', with: email
end

def fill_password(password)
  fill_in 'user_password', with: password
end

Creating a Page Specific Class

Now, using the above SignIn page class we can build up another page class that can be used in a more targeted spec:

class UserProfile < Page
  def self.visit(user, logged_in = false)
    if logged_in
      page = SignIn.visit
      page.sign_in(user.email, user.password)
      page.visit '/users/edit'
      new
    else
      page.visit '/users/edit'
      new
    end
  end
end

Use in RSpec

Here is an example of all of the above coming together in an actual spec:

RSpec.describe 'User Profile', type: :feature do
  let :user { FactoryBot.create(:user) }

  it 'allows allows a signed in user to view their profile' do
    profile_page = UserProfile.visit(user, logged_in: true)
    expect(profile_page).to have_content('Profile')
  end

  it 'does not allow profile to be accessed without signing in' do
    profile_page = UserProfile.visit
    expect(profile_page).to have_content('You are not authorized to view this page.')
  end
end

As seen in the example above the spec is easy to read and it is obvious what is going on. This also has the added benefit of allowing the sign-in functionality to be changed and only needing to update the methods in one place in the SignIn page object.

Conclusion

The examples here have been simplified so that the actual mechanics of building up the page objects could be discussed plainly. In an actual application these page objects can get to be pretty complicated. The good part about this pattern is that the complexity can be contained in one place and is not spread out across several specs that are unconnected.

While this article uses Capybara and RSpec this pattern can be applied in many testing frameworks with the same benefits.

Loved the article? Hated it? Didn’t even read it?

We’d love to hear from you.

Reach Out

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

More Insights

View All