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.