Last Updated:

Ruby Page Objects for Capybara Connoisseurs

First, I'll tell you a short topic. It's a design pattern for encapsulating the markup and interaction of pages, especially for refactoring your characteristics. It's a combination of two very common refactoring methods— Check Out Class and Extract Method —which shouldn't happen at the same time because you can gradually move on to retrieving an entire class through a new Page Object.

This method allows you to write high-level characteristics that are very expressive and DRY. In a way, these are acceptance tests with the language of the application. You may ask, aren't the specs written in Capybara high-level and expressive? Of course, for developers who write code on a daily basis, Capybara's specifications read just fine. Are they dry out of the box? Not quite – actually, of course not!

The 'ruby 'M creates mission' function makes the script 'successful' does sign_in_as 'M@mi6.com'

"'ruby'M function marks the mission as completed' do script 'success' do sign_in_as 'M@mi6.com'

When you look at these examples of feature specifications, where do you see opportunities to improve that reading and how can you extract information to avoid duplication? Also, is this high level enough for easy modeling of user stories and for non-technical stakeholders to understand?

In my opinion, there are several ways to improve this and make everyone happy: developers who can avoid fuss about the details of interacting with the DOM when applying OOP, and other team members who don't code have no problem navigating between user stories and these tests. The last point is certainly nice to have, but the most important benefits are mostly to make your DOM-interacting specifications more robust.

Encapsulation is a key concept with Page objects. When you write specifications for your features, you benefit from a strategy to extract behaviors that affect the testing process. As for quality code, you want to capture interactions with specific sets of elements on your pages, especially if you've stumbled upon repetitive patterns. As your application grows, you need/need an approach to avoid extending this logic to all of your specifications.

"Well, isn't that overkill? Capybara is a great reader!

Ask yourself a question: why don't you have all the details of the HTML implementation in one place, having more stable tests? Why shouldn't UI experience tests be of the same quality as tests for application code? Are you sure you want to stop there?

Due to daily changes, your Capybara code is vulnerable to ubiquity distribution – it introduces possible breakpoints. Let's say a designer wants to change the text on a button. It doesn't matter, right? But do you want to adapt to this change in one central shell for this element in your specifications, or do you prefer to do it everywhere? I thought so!

There are many possible refactorings for your component specifications, but Page Objects offers the purest abstractions for encapsulating user behavior for pages or more complex threads. You don't need to simulate the entire page(s), although focus on the basic bits that are needed for user streams. No need to overdo it!

Before we get to the heart of the matter, I'd like to take a step back for people who are new to the entire testing business, and clarify some of the jargon that is important in this context. People more familiar with TDD won't miss much if they skip ahead.

What are we talking about here? Acceptance testing is usually done at a later stage of projects to assess whether you have created something of value for your users, the product owner, or other stakeholders. These tests are usually conducted by customers or your users. This is a kind of check whether the requirements are met or not. There is something like a pyramid for all sorts of layers of testing, and acceptance tests are at the very top. Because this process often involves non-technical people, high-level language for writing these tests is a valuable asset for communicating back and forth.

On the other hand, the specifications are slightly lower in the food testing chain. Much higher-level than unit tests that focus on the technical details and business logic of your models, feature specifications describe flows between and between your pages.

Tools like Capybara will help you avoid this manually, which means you rarely have to open a browser to test the data manually. With this kind of testing, we'd like to automate these tasks as much as possible and test the browser experience when writing statements for pages. By the way, you don't use getputpost, or delete the way you use request specifications.

Feature characteristics are very similar to acceptance tests – sometimes I feel like the differences are too blurred to really care about terminology. You write tests that validate your entire app, which often involves a multi-step flow of user actions. These interaction tests show whether your components are working in harmony when they are put together.

In the land of Ruby, they are the protagonists when we deal with Page objects. The feature specifications themselves are already very expressive, but they can be optimized and cleaned up by separating their data, behavior, and markup into a separate class or classes.

I hope that clarifying this vague terminology will help you see that having Page Objects is a bit like testing at an acceptance level when writing feature specifications.

Maybe we should get through this very quickly. This library describes itself as "an acceptance testing framework for web applications." You can simulate user interaction with your pages using a very powerful and user-friendly domain-specific language. In my personal opinion, RSpec paired with Capybara offers the best way to write your technical specifications at the moment. It allows you to visit pages, fill out forms, click on links and buttons, and search for markup on your pages, and you can easily combine all kinds of these commands to interact with your pages during your tests.

Basically, you don't have to open the browser yourself to test this stuff manually most of the time, which is not only less elegant, but also much more time-consuming and error-prone. Without this tool, the process of "external testing" – you translate your code from high-level tests to your module-level tests – would be much more painful and perhaps thus more ignored.

In other words, you start writing these functional tests based on your user stories, and from there you go down the rabbit hole until your unit tests provide the coverage you need for your specs. After that, when your tests turn green, the game will naturally start again and you'll go back to continue testing the new feature.

Let's look at two simple examples of feature specifications that allow M to create secret missions that can then be completed.

In the markup, you have a list of missions, and a successful completion creates an additional class, completed on that particular mission. Simple things, right? As the first approach, I started with small, very common refactorings that extract common behavior into methods.

Specifications / Features / m_creates_a_mission_spec.rb

“ `ruby require ‘rails_helper’

"M creates mission" feature makes the scenario "successful" makes sign_in_as "M@mi6.com"

def create_classified_mission_named (mission_name) visit the "Create Mission" missions_path click_on, fill in the "Mission Name", using: mission_name click_button "Submit" end

def agent_sees_mission (mission_name) Expected (page) .to have_css 'li.mission-name', text: mission_end_title

def sign_in_as visit the 'Email' root_path fill_in, from: email click_button 'Submit' end end " '

Specifications / Features / agent_completes_a_mission_spec.rb

“ `ruby require ‘rails_helper’

"M marks mission as complete" function, successfully complete the script, execute the sign_in_as "M@mi6.com" function

def create_classified_mission_named (mission_name) visit the "Create Mission" missions_path click_on, fill in the "Mission Name", using: mission_name click_button "Submit" end

def agent_sees_completed_mission (mission_name) expected (page) .to have_css 'ul.missions li.mission-name.completed', text: mission_title end

def sign_in_as visit the 'Email' root_path fill_in, from: email click_button 'Submit' end end " '

While there are certainly other ways to work with things like sign_in_ascreate_classified_mission_named, etc., it's easy to see how quickly these things can start sucking and accumulating.

I think user interface-related specifications often don't get the GS treatment they need/deserve. They have a reputation for being a provider of too little money, and of course, developers don't really like the times when they have to touch markup a lot. In my opinion, it's even more important to dry out these specifications and make them fun to work with by adding a couple of Ruby classes.

Let's do a little magic trick where I hide the implementation of the Page Object for now and show only the end result applied to the function specifications above:

Specifications / Features / m_creates_a_mission_spec.rb

“ `ruby require ‘rails_helper’

function "M creates a mission", execute the script "successfully", perform the sign_in_as "M@mi6.com". Visit missions_path mission_page = Pages :: Missions.new

Specifications / Features / agent_completes_a_mission_spec.rb

“ `ruby require ‘rails_helper’

"M marks mission as complete", "execute scenario" successfully, "complete sign_in_as" function, M@mi6.com, visit missions_path mission_page = Pages :: Missions.new

Not too bad at reading, eh? You basically create expressive wrapper techniques on your Page objects that allow you to deal with high-level concepts – instead of constantly messing around with the guts of your markup. Your extracted methods are doing this dirty work now, and thus the shotgun operation is no longer your problem.

In other words, you encapsulate most of the noisy, DOM-interacting code. However, I have to say that sometimes there are enough reasonably extracted methods in your function specifications, and they read a little better, as you can avoid working with Page Object instances. Either way, let's look at the implementation:

Functions / Support / Function / Pages / missions.rb

"ruby module Missions class pages include Capybara :: DSL

What you're seeing is a regular old Ruby object. Page objects are essentially very simple classes. You don't usually create Page Objects instances with data (when, of course, you can), and you're basically creating a language through an API that can be used by a user or a non-technical team member. When you think about naming your methods, I think it's a good tip to ask yourself the question: How would the user describe the flow or the actions taken?

Perhaps I should add that without capybara enabled, the music stops pretty quickly.

ruby include Capybara::DSL

You're probably wondering how these custom mappers work:

'waiting for ruby (page). to have_mission_ Named 'Project Moonraker' waiting (page) .to have_completed_mission_name 'Project Moonraker'

def has_mission_name? (mission_name)... The end

def has_completed_mission_named? (mission_name)... end “ `

RSpec generates these custom mappings based on predicate methods on Page objects. RSpec converts them by deleting ? and the changes must have . Boom, matches from scratch without much fluff! A little magic, I'll give you that, but a good kind of magic, I'd say.

Since we've parked our Page object at specs/support/features/pages/missions.rb, you'll also need to make sure the following isn't commented out in spec/rails_helper.rb.

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

If you're a NameError with NameError with uninitialized constant Pages, you'll know what to do.

If you're wondering what happened to the sign_in_as method, I extracted it into the module at spec/support/sign_in_helper.rb and told RSpec to enable the module. This has nothing to do with page objects directly, it just makes sense to store test functionality, such as sign in, in a more accessible way than through a page object.

Specification / Support / sign_in_helper.rb

ruby module SignInHelper def sign_in_as(email) visit root_path fill_in 'Email', with: email click_button 'Submit' end end

And you need to tell RSpec that you want to access this helper module:

Specifications / spec_helper.rb

“ `ruby... require ‘support / sign_in_helper’

RSpec.configure do | config | config.include SignInHelper... end “ `

Overall, it's easy to see that we've managed to hide Capybara's features – such as finding elements, clicking on links, etc. We can now focus on functionality rather than the actual markup structure that is now encapsulated in the page object. – The DOM structure should be the least of your problems when you're testing something as high as the function specifications.

Installation data, such as factory data, refers to the specification, not to page objects. Also, claims are probably better placed outside of your Page objects to achieve a separation of interests.

There are two different points of view on this topic. Proponents of placing claims in Page objects say this helps avoid duplicate claims. You can provide better error messages and achieve a better "talk, don't ask" style. On the other hand, proponents of object objects without statements argue that it is better not to mix responsibilities. Providing access to page data and approval logic are two separate issues that lead to bloated page objects when mixed. The responsibility for the page object is access to the state of the pages, and the assertion logic belongs to the specifications.

Components represent the smallest units and are more focused– for example, an object of form.

Pages combine more of these components and are abstractions of the full page.

The experience, you guessed it, covers the entire flow of potentially many different pages. They're at a higher level. They focus on the flow that the user experiences when interacting with different pages. A checkout flow consisting of several steps is a good example to think about.

It's a good idea to apply this design pattern a little later in the project lifecycle—when you've accumulated a bit of complexity in function specifications, and when you can identify repetitive patterns, such as DOM structures, extracted methods, or other commonalities that correspond to your pages.

So, you probably shouldn't start writing page objects right away. You approach these refactorings gradually as the complexity and size of your app/tests grow. Duplicates and refactorings that need home improvement with Page Objects will be easier to spot over time.

I recommend starting by extracting the methods in your feature specifications locally. Once they reach critical mass, they will look like obvious candidates for further extraction, and most of them will likely fit the Page Objects profile. Start small, because premature optimization leaves unpleasant bite marks!

Page objects give you the ability to write clearer specifications that read better and are generally much more expressive because they are of a higher level. In addition, they offer a good abstraction for anyone who likes to write OO code. They hide the specifics of the DOM, and also allow you to have closed methods that do the dirty work while being inaccessible to the public API. The extracted techniques in your specifications don't offer the same luxury. Page Objects APIs do not need to share the smallest details of a capybara.

For all scenarios where project implementations change, your descriptions of how your app should work shouldn't change when using page objects—your feature specifications focus more on user-level interactions and don't care too much about the specifics of the DOM implementation. Because changes are inevitable, Page objects become critical when extending applications, and also help to understand when the size of the application significantly increases complexity.