Clean Up After Your Capybara

Mike Mazur
Neo Innovation | Ideas
8 min readOct 17, 2014

--

Use page objects for cleaner and less brittle integration specs

Avoid the tangled web of test code and markup. Use page objects for integration testing. They make your specs clearer, more effective at communicating intent. The coupling between specs and markup is localized in the page objects, making the feature test suite less effort to maintain.

If you’re encountering page objects for the first time it may not be clear how to write your tests. They’re an extra layer between the test you’re writing and the markup you’ll need to write.

In this post I’ll demonstrate my approach. My strategy is to write the API I want first, then let the failing test tell me what to implement next. It’s similar to tests driving the implementation when practicing TDD.

Show Me How It’s Done

Let’s work through an example (code is on GitHub). The example app has a list of blog post titles with a comment count next to each. Clicking on the title displays the post, existing comments, and a form where the reader can post new comments.

For users already signed in, submitting a new comment should increment the comment count visible on the index page. If a comment is submitted by someone not already signed in, a dialog pops up where they can do so. Here’s how some vanilla Capybara/RSpec specs might look like for this behavior:

feature "User can comment", js: true do
background do
Post.create! title: 'Yakety Yak'
User.create! username: 'hammerhead'
end

scenario "leaving a comment persists the comment and increases the comment count" do
visit '/sign_in' # we're faking user authentication, signs us in as first user
visit '/posts'
old_comment_count = comment_count_for 'Yakety Yak'

click_on 'Yakety Yak'
fill_in 'new-comment', with: "Don't talk back"
click_on 'Submit comment'
expect(page).to have_content "Don't talk back"

click_on 'Back to all posts'
comment_count = comment_count_for 'Yakety Yak'
expect(comment_count).to eq old_comment_count + 1

click_on 'Yakety Yak'
expect(page).to have_content "Don't talk back"
end

scenario "logged out user can sign in when they submit a new comment" do
visit '/posts'
click_on 'Yakety Yak'
fill_in 'new-comment', with: "Don't talk back"
click_on 'Submit comment'

within '#sign-in-dialog' do
fill_in 'Login', with: 'hammerhead'
fill_in 'Password', with: 'deploytheyak'
click_on 'Sign in'
end

expect(page).to have_content "Don't talk back"

visit page.current_path
expect(page).to have_content "Don't talk back"
end

def comment_count_for post_title
# TODO: find the count and return as integer
4
end
end

These tests are written at a fairly low level of abstraction. Submitting a new comment takes two “instructions:” fill in a text area and click Submit. Do this enough and soon it’ll be difficult to pick out meaningful actions from the jumble of `fill_in`s and `click_on`s. The `comment_count_for` helper function mitigates this somewhat and it’s a step in the right direction.

The specs are also coupled to the markup. When you make changes to the markup – the ID of the comment text area, the text on the submit button, or how a user navigates from post to post listing – you’ll need to change all affected specs.

Don’t Repeat Yourself

The markup in each test can be seen as duplication. We should DRY it up! We can do that by placing it in classes representing our app’s pages. What would a class like that look like? Let’s start by writing down how we’d like to interact with them:

scenario “leaving a comment persists the comment and increases the comment count” do
visit ‘/sign_in’ # we’re faking user authentication, signs us in as first user
blog_index_page = BlogIndex.visit
old_comment_count = blog_index_page.comment_count_for ‘Yakety Yak’
blog_post_page = blog_index_page.read ‘Yakety Yak’
blog_post_page.create_comment “Don’t talk back”
expect(blog_post_page).to have_content “Don’t talk back”
blog_index_page = blog_post_page.navigate_to_blog_index
comment_count = blog_index_page.comment_count_for ‘Yakety Yak’
expect(comment_count).to eq old_comment_count + 1
blog_post_page = blog_index_page.read ‘Yakety Yak’
expect(blog_post_page).to have_content “Don’t talk back”
end

We introduced the `BlogIndex` class which represents the blog index page. `BlogIndex.visit` replaced Capybara’s `visit` in our test and returns a `BlogIndex` instance. There’s also `BlogIndex#read` which navigates to a specific blog post given its title and returns an object representing the blog post show page. And so on: interacting with a page is done by invoking methods on the page object, and navigating to another page returns a new page object representing the new page.

Running this test fails with `NameError: uninitialized constant BlogIndex` and we immediately know what to do next. In `spec/support/pages/blog_index_page.rb` we write:

class BlogIndex
end

Running the test again we get ``NoMethodError: undefined method `visit’ for BlogIndex:Class``, so we write it:

class BlogIndex
def self.visit
page.visit ‘/posts’
new
end
end

`page.visit` invokes Capybara’s visit method. `BlogIndex` needs to include the `Capybara::DSL` module, but so will all page objects. Let’s create a base class in `spec/support/pages/page.rb`:

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

We extend and include because we’ll need access to the DSL methods in class and instance methods. Let’s flush out the rest of our `BlogIndex` page object:

class BlogIndex < Page
def self.visit
page.visit ‘/posts’
new
end
def comment_count_for post_title
# TODO: find the count and return as integer
4
end
def read title
click_on title
BlogPost.new
end
end

We also moved our dummy implementation of `comment_count_for` to `BlogIndex`.

Here’s the `BlogPost` page object:

class BlogPost < Page
def self.visit post_id
page.visit “/posts/#{post_id}”
new
end
def create_comment text
fill_in ‘new-comment’, with: text
click_on ‘Submit comment’
end
def navigate_to_blog_index
click_on ‘Back to all posts’
BlogIndex.new
end
end

Overall, nothing special, we just moved the Capybara code we had before into the page objects. But now our tests are written at a higher level of abstraction. Someone reading the specs no longer has to group and parse calls to `fill_in` and `click_on` to understand what the user is doing. Methods like `read` and `create_comment` make that clear. The tests are no longer coupled to the markup, either; we localized the coupling in the page objects.

And Repeat

Now let’s do the other spec:

scenario “logged out user can sign in when they submit a new comment” do
blog_post_page = BlogIndex.visit.read ‘Yakety Yak’
blog_post_page.create_comment “Don’t talk back”
SignInDialog.new.sign_in_as ‘hammerhead’, ‘deploytheyak’
expect(page).to have_content “Don’t talk back”
blog_post_page.refresh
expect(page).to have_content “Don’t talk back”
end

class SignInDialog < Page
def sign_in_as username, password
fill_in ‘Login’, with: ‘hammerhead’
fill_in ‘Password’, with: ‘deploytheyak’
click_on ‘Sign in’
end
end

`SignInDialog` interfaces with a modal that pops up with a sign in form when an anonymous user attempts to leave a comment. It’s not exactly a page, it represents a fragment of a page. If the page had a login form elsewhere, the test could fail because Capybara finds multiple fields labeled Login. We’ll see how to address that shortly.

We also introduced `BlogPost#refresh` which is more descriptive than `visit current_path`. It’s something probably useful on any page, so we’ll add it to the `Page` base class:

class Page
extend Capybara::DSL
include Capybara::DSL
def refresh
page.visit page.current_path
end
end

`Page` is a good place for interactions common to all pages, such as navigating around using a site-wide header, logging out, and so on.

It’s All In The Details

Let’s implement retrieving the comment count for a blog post. Our index page renders the following markup for each post:

<div class=”post”>
<h3><a href=”/posts/1">This is the blog post title</a></h3>
<span class=”post-stats”>14 comments</span>
</div>

Finding the post count might look something like this:

def comment_count_for post_title
post_containers = all(‘.post’)
post_container = post_containers.detect { |c| c.find(‘h3').text == post_title }
post_stats = post_container.find(‘.post-stats’).text
post_stats.gsub(‘comments’, ‘’).to_i
end

We iterate over all elements with a `Post` class, stopping at the first one containing the title provided. Within that fragment we find the part that has the comment count, clean it up and return an integer. For the markup sample above the result is 14.

We can create an object that represents this page fragment for the blog post and its stats:

class BlogPostAndStats
def initialize element
@element = element
end
def post_title
@element.find(‘h3').text
end
def comment_count
@element.find(‘.post-stats’).text.gsub(‘comments’, ‘’).to_i
end
end

This changes our algorithm slightly:

def comment_count_for post_title
post_containers = all(‘.post’).map { |p| BlogPostAndStats.new p }
post_container = post_containers.detect { |c| c.post_title == post_title }
post_container.comment_count
end

Instead of dealing with Capybara nodes and the specific markup that makes up a blog post listing entry, we’re interacting with a domain object.

We now have two classes which represent fragments of our page: `SignInDialog` and `BlogPostAndStats`. Most likely there will only be one sign in dialog on a page, but a `BlogPostAndStats` fragment appears multiple times. To ensure that a fragment is interacting with the correct portion of the DOM, we provide a “root” node to the fragment on construction and scope Capybara’s DSL calls to that node. We can use the same technique to make sure `SignInDialog#sign_in_as` always interacts with the right DOM node. Both `SignInDialog` and `BlogPostAndStats` will have an `initialize` method which takes an element. This calls for a base class:

class PageFragment
def initialize element
@element = element
end
end

Now we can make `SignInDialog` inherit from `PageFragment` and scope the calls to `fill_in` and `click_on` to the root node:

class SignInDialog < PageFragment
def sign_in_as username, password
@element.fill_in ‘Login’, with: ‘hammerhead’
@element.fill_in ‘Password’, with: ‘deploytheyak’
@element.click_on ‘Sign in’
end
end

We just need to make sure to pass the right node when creating a `SignInDialog` instance.

It’s Your Turn

You’ve now seen how to use page objects in your tests and how to design them: first write the interactions you want in your test, then let the failures tell you what to implement next. Your specs end up clearer and communicate intent better. The coupling between specs and markup is localized to the page objects, making it quicker to change the markup and maintain the feature test suite.

As a bonus, we now have a collection of page objects to iterate on as we layer functionality onto the app. For example, let’s say you’re building a feature that automatically changes a user’s status to VIP once they have left 10 comments. The integration specs for that belong in a separate file, but all the plumbing for interacting with comments from the specs already exists.

In this example I roll my own page objects. There are gems which provide a starting point, such as SitePrism, tooth and test-page. While I didn’t find them too helpful, you might, so check them out!

Also published at neo.com. Images CC BY 2.0 by jasoneppink, lorentey and brent_nashville.

--

--

Mike Mazur
Neo Innovation | Ideas

VP Engineering @BBM Singapore. Passionate about highly efficient teams and quality code.