Video

Want to see the full-length video right now for free?

Notes

On this week's episode, Chris is again joined by Josh Clayton, Boston Development Director and TDD master, this time to discuss the power of page objects for cleaning up feature specs.

What Are Page Objects

Page objects encapsulate and abstract some of the specifics of page markup and interactions, allowing us to write our feature specs at a higher level and maintain them more easily.

Page objects can focus on specific elements on a page, can describe a whole page, or can even describe an interaction flow through an app that might span multiple pages.

Why Use Page Objects

Page objects are both simple to implement and surprisingly effective at taming duplication and spec complexity. They are one of the best tools for producing readable, expressive, and maintainable feature specs.

Refactor out Duplicated Setup Code

Page objects are ideally suited to removing duplication across feature specs. They provide a central, collected place to put helpers, references to markup, etc.

Ensure Feature Specs Remain High Level

Another great benefit of page objects is that they help you to pull markup out of your feature specs, allowing the specs to be written at a high level similar to how a user would describe interacting with the app.

Todo App Example

To get a sense of the cleanup that can come with introducing a page object, we can take a look at [the commit where Josh refactors to use a page object][] in the feature specs for the todo application built in the [Test-Driven Rails trail][].

Of particular note is where the mark_complete method was introduced:

- within "li:contains('Buy eggs')" do
-   click_on "Mark complete"
- end
+ todos_page.mark_complete "Buy eggs"

The original version was much less direct and relied on particular tags and a complex selector and Capybara methods to run, but the page object hides all of this cruft and describes exactly the action the user would take in about the most direct way possible.

Likewise, the assertion in the "completes todo" example was also cleaned up significantly thanks to the page object:

- expect(page).to have_css "ul.todos li.completed", text: "Buy eggs"
+ expect(todos_page).to have_completed_todo_titled "Buy eggs"

Again, the page object allows us to write the high level user-focused spec we want, while providing a place to hide the lower level implementation details.

[Test-Driven Rails trail]: https://upcase.com/test-driven-rails [the commit where Josh refactors to use a page object]: https://github.com/joshuaclayton/testing_rails/commit/2fa4a82c349bff0084e583373c7658af8e9e99ec

Using Factory Girl

While we generally want to keep the feature spec focused at the user-interacting-with-a-page level, we are OK with using the [FactoryGirl][] setup methods like create to build the needed context. This fits with the overall goal of describing a page interaction using the language that a user would.

Generally you'll want to have at least one feature spec exercise the UI to create a given object, but assuming you have that in place and you feel confident in the behavior with that test coverage, we're fine with using the FactoryGirl methods to keep other specs more focused on the unique workflow they are testing, rather than needing to repeat the interactions tested in the spec covering the creation of the object via the UI.

[FactoryGirl]: https://github.com/thoughtbot/factory_girl

Example Page Object

Check out the [Todos][] and the [NewTodo][] page object discussed in the video to see the complete implementation of these two page objects.

[Todos]: https://github.com/joshuaclayton/testing_rails/blob/2fa4a82c349bff0084e583373c7658af8e9e99ec/spec/support/features/pages/todos.rb [NewTodo]: https://github.com/joshuaclayton/testing_rails/blob/2fa4a82c349bff0084e583373c7658af8e9e99ec/spec/support/features/pages/new_todo.rb

Capybara DSL

The primary mechanism for building a page object is to include the Capybara::DSL module. This exposes all of the usual Capybara methods like visit and click_on within the methods of our page object.

module Pages
  class Todos
    include Capybara::DSL

    # all your methods here
  end
end

Predicate Methods

Working with Rspec, we can define custom predicate methods for use in Rspec assertions. For instance, given we have an assertion like:

expect(todos).to have_todo_title "Buy Eggs"

Rspec will map the have_todo_title and the arguments to the corresponding has_todo_title? method on our page object. Within these custom predicate methods, we can use the existing Capybara methods on node objects like has_css? and has_content? to map the higher level page object assertion to the lower level Capybara implementation.

Using Page Objects to Hide Implementation

When cleaned up, it's common for a page object to hide all of the finder methods like todos_list in their private API and only expose a set of methods for interacting or asserting against the page. This gets us all the closer to the ideal of feature specs being written from the user's perspective.

Using Translations Within Page Objects

Page objects also provide a great place to collect any references to i18n translation strings. We have a separate [Weekly Iteration episode on Internationalization][] that provides even more detail on the topic.

[Weekly Iteration episode on Internationalization]: https://upcase.com/videos/Internationalization

Using Formulaic Within Page Objects

Another tool we can use in our page objects is [Formulaic][]. Formulaic is a thoughtbot gem that streamlines filling out forms with Capybara. Just like we can include the Capybara::DSL, we can also include the Formulaic::DSL in our page objects to gain access to the fill_form_and_submit method:

module Pages
  class Todos
    include Capybara::DSL
    include Formulaic::DSL

    # all your methods here
  end
end

[Formulaic]: https://github.com/thoughtbot/formulaic

Different Types of Page Objects

Again, the term "page object" is a slight misnomer since we actually use a combination of three main types of page objects

  • Component - these objects wrap a portion of the page, such as a "menu" or a single todo on the page.
  • Page - these represent a whole page, possibly composing multiple Component objects.
  • Experience - these objects represent a flow through an application, for instance the checkout flow on an ecommerce site, which might be composed of multiple pages, and thus multiple page objects.

What's nice is that each of these can be gradually refactored to, first extracting a simple object from the feature specs, then slowly extracting out into more and more page objects.

Page Objects are Something You Refactor To

While page objects are a great addition to your spec support files, they are generally better as something you refactor to than something you start with out the gate. You want to create a few feature specs and see the patterns and sources of duplication that emerge, then extract those to helper methods, then eventually move into page objects.

Regardless of when you introduce them, we highly recommend giving page objects a shot if you've yet to use them. They are one of the most effective tools we have for fighting complexity in feature specs and are an invaluable part of our testing workflow these days, and we're sure you'll find the same.