Want to see the full-length video right now for free?
Sign In with GitHub for Free AccessOn 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.
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.
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.
Page objects are ideally suited to removing duplication across feature specs. They provide a central, collected place to put helpers, references to markup, etc.
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.
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.
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.
Check out the Todos and the NewTodo page object discussed in the video to see the complete implementation of these two page objects.
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
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.
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.
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.
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
Again, the term "page object" is a slight misnomer since we actually use a combination of three main types of 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.
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.