Ruby
Article

Testing Page Objects with SitePrism

By Benjamin Tan Wei Hao

Prism Spectrum Illustration on Blue Background

In this article, I am going to talk about SitePrism, a testing tool that I have recently learned about and been enjoying. Here is a description of SitePrism from its’ creators:

SitePrism gives you a simple, clean and semantic DSL for describing your site using the Page Object Model pattern, for use with Capybara in automated acceptance testing.

Before we go into SitePrism, let’s talk about page objects, because page objects are central to understanding the philosophy behind SitePrism.

A Page Objects Primer

Page Objects encapsulate the implementation details of the page (in our example, an HTML document) and expose a specific API that allows you, the tester, to test elements on the page. This encapsulation of details make page objects especially handy as a testing tool.

Let’s illustrate this with an example. Here, we have a page object that inherits from the SitePrism::Page class:

class LoginPage < SitePrism::Page
  element :username_field, "input[name='username']"
  element :password_field, "input[name='password']"
  element :flash, "div.flash"

  def log_in(username, password)
    username_field.set(username)
    password_field.set(password)
    click_on('Log In')
  end
end

class ProfilePage < SitePrism::Page
  element :flash, "div.flash"

  def flash_message
    flash.text
  end
end

Let’s see how we can use our example page object:

feature 'Login' do
  let(:login_page) { LoginPage.new }

  before do
    login_page.load
  end

  scenario 'a successful login' do
    profile_page = login_page.log_in('admin', 'password')
    expect(profile_page.flash_message).to eq("Ohai! Welcome Admin.")
  end
end

The above code snippet illustrates how we can use SitePrism’s page objects in our RSpec feature tests. Ready to learn more?

Getting Site Prism Into Your Project

Installing Site Prism is a painless affair. In your Gemfile:

group :test do
  # ...
  gem 'rspec-rails', '~> 3.0.0'
  gem 'capybara-rails'
  gem 'site_prism'
  # ...
end

Note that for the example project, we are going to use RSpec 3. Also, you will need Capybara in order to use SitePrism.

Working with the Example Project

The best way to learn about SitePrism (and everything else, really) is to have some hands on experience. For this, I have prepared an example application that you can use to follow along this article.

The sample application is a Spree e-commerce store. At this point of writing, we are using the latest 2.3-stable branch and running it on Rails 4.1.4.

To get the project:

$ git clone git@github.com:benjamintanweihao/rails_store.git
$ cd rails_store
$ bundle install

The Task: Testing Out Search

In order to get our hands dirty and feet wet with SitePrism, we are going to test the search function of the store.

Some of the portions of the test have Spree-specific code. Don’t worry, I will point them out. In most cases, you can safely ignore them. The sample Spree store makes for a realistic platform to practice our testing skills.

Let’s go through the implementation of the HomePage page object. I store all my page object classes in a spec/support/pages directory. You are free to choose your own conventions. Just make sure that the directory is included in the tests.

Here’s how the page looks like:

home page

HomePage is subclassed from SitePrism::Page. element is used to specify one DOM element, while elements is used to refer to a collection of DOM elements. Here, we pass in a symbol (for example, :products), and then the CSS selector to target the various elements on the page.

Here’s the initial implementation of the HomePage page object:

class HomePage < SitePrism::Page
  elements :products,        "[data-hook='products_list_item']"
  elements :product_links,   "[data-hook='products_list_item'] a"
  element  :search_field,    "input#keywords"
  element  :search_dropdown, "select#taxon"
end

Let’s define the search_for method, which wraps actions such as selecting the drop down, filling in the search field, and clicking on the button. We are using Capybara’s methods, so if you are unfamiliar with them, you should definitely consult the documentation.

# rails_store/spec/support/pages/home_page.rb
class HomePage < SitePrism::Page
  # elements previously defined
  def search_for(query, scope='All departments')
    search_dropdown.select(scope)
    search_field.set(query)
    click_on('Search')
  end
end

Before we go carry on, I want to add another line to the code:

# rails_store/spec/support/pages/home_page.rb
class HomePage < SitePrism::Page
  # elements previously defined
  def search_for(query, scope='All departments')
    search_dropdown.select(scope)
    search_field.set(query)
    click_on('Search')
    SearchResults.new # <--------- Added this! -----
  end
end

What does this buy us? Having a method in the page object return another page object is a nice way to express the user’s journey through the site.

Let’s define the search results page:

class SearchResults < SitePrism::Page
  elements :products, "[data-hook='products_list_item']"
end

The search results page has a single elements that returns a collection of the elements that matches the "[data-hook='products_list_item']" selector.

Let’s take a look with the feature specs for more details:

#  rails_store/spec/features/home/search_products_spec.rb
require 'rails_helper'

feature 'Product Search' do
  # factories to load products into the test database
  include_context 'custom products'

  let(:home_page) { HomePage.new }

  before do
    home_page.load
  end

  scenario 'a search term is entered' do
    search_results = home_page.search_for('jersey')
    expect(search_results.products.size).to eq(3)
  end

  scenario 'search limited by dropdown' do
    search_results = home_page.search_for('mug', 'Categories')
    expect(search_results.products.size).to eq(1)
  end
end

We first set up the HomePage instance like so:

let(:home_page) { HomePage.new }

before do
  home_page.load
end

Note that we have to call #load on the page object in other to signal Capybara to load the page in a headless browser. Next, our first scenario:

scenario 'a search term is entered' do
  search_results = home_page.search_for('jersey')
  expect(search_results.products.size).to eq(3)
end

It tests a search term being entered into the search bar. Since it takes in only one argument (jersey), the selector box will be set to All Departments. The search button is clicked. Finally, a new SearchResult instance is returned.

Here’s an important point. Going by the user flow, once a user performs a search, she should be directed to another page. This is represented by the SearchResult instance. Since we have specified :products in the SearchResult class, we can use that in our assertion like so:

expect(search_results.products.size).to eq(3)

Learning More

I have only scratched the surface of what SitePrism can do. The SitePrism documentation is well-written. Spend 5 minutes reading those docs and I bet you will agree with me.

More Practice: Testing out the Home Page

In the GitHub repository, you will find rails_store/spec/features/home/list_products_spec.rb. Look at the methods used by the page object, and try to implement your own page object to make the tests pass.

scenario 'enters home page' do
  expect(home_page.product_permalinks).to include('ruby-on-rails-mug',
                                                  'ruby-on-rails-tote')
end

scenario 'clicks on a product' do
  product_link = home_page.product_links.first
  product_page = home_page.click_on_product_link(product_link)

  expect(product_page).to be_displayed

  expect(product_page.title).not_to be_empty
  expect(product_page.description).not_to be_empty
  expect(product_page.price).not_to be_empty
end

If you get stuck, you can always peek at the implementation I have at rails_store/spec/support/pages/home_page.rb.

Final Thoughts

I was never a big fan of Cucumber. I think SitePrism makes feature tests way more fun, and strikes a nice balance between readability and conciseness. I particularly like that I can represent a HTML page using an object, where interactions to the page are exposed through methods.

Another thing I like about page objects is the encapsulation they provide. For example, should I decide that I want to change the CSS selector of input#keywords to input#search, I would just need to modify the CSS selector in the respective element method.

That said, I think this is mainly a matter of personal taste. I’ve been enjoying using SitePrism on a couple of projects and encourage you to at least give it a shot.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.