How to clear up obscure Rails tests using Page Objects

by Jason Swett,

The challenge of keeping test code clean

The hardest part of a programmer’s job isn’t usually figuring out super hard technical problems. The biggest challenge for most developers, in my experience, is to write code that can stand up over time without collapsing under the weight of its own complexity.

Just as it’s challenging to keep a clean and understandable codebase, it’s also challenging to keep a clean and understandable test suite, and for the same exact reasons.

If I look at a test file for the first time and I’m immediately able to grasp what the test is doing and why it’s doing it, then I have a clear test. The test has a high signal-to-noise ratio. That’s good.

The opposite scenario is when the test is full of noise. Perhaps the test contains so many low-level details that the high-level meaning of the test is obscured. The term for this condition, this “test smell”, is Obscure Test. That’s bad. If the test code is obscure when it could have been clear, that creates extra head-scratching time.

One tool that can be used to help make Obscure Tests more understandable is the concept of a Page Object.

What a Page Object is and when it’s helpful

As a preface: Page Objects are only relevant when dealing with integration tests—that is, tests that interact with the browser. Page Objects don’t come into the picture for model tests or anything else that doesn’t interact with a browser.

A Page Object is an abstraction of a component of a web page. For example, I might create a Page Object that represents the sign-in form for a web application. (In fact, virtually all my Page Objects represent forms, since that where I find them to be most helpful.)

The idea is that instead of using low-level, “computerey” commands to interact with a part of a page, a Page Object allows me to use higher-level language that’s more easily understandable by humans. Just as Capybara is an abstraction on top of Selenium that’s clearer for a programmer to read and write than using Selenium directly, Page Objects can be an abstraction layer on top of Capybara commands that’s clearer for a programmer to read and write than using Capybara directly (at least that’s arguably the case, and only in certain situations, and when done intelligently).

Last note on what a Page Object is and isn’t: I personally started with the misconception that a Page Object is an abstraction of a page. According to Martin Fowler, that’s not the idea. A Page Object is an abstraction of a certain part of a page. There’s of course no law saying you couldn’t create an abstraction that represents a whole page instead of a component on a page, but having done it both ways, I’ve found it more useful to create Page Objects as abstractions of components rather than of entire pages. I find that my Page Objects come together more neatly with the rest of my test code when that’s the case.

A Page Object example

I’m going to show you an example of a Page Object by showing you the following:

  1. A somewhat obscure test
  2. A second, cleaner version of the same test, using a Page Object
  3. The Page Object code that enables the second version of the test

Here’s the obscure test example. What exactly it does is not particularly important. Just observe how hard the test code is to understand.

The obscure test

RSpec.describe "Grocery list", type: :system do
  let!(:banana) { create(:grocery_item) }

  describe "deleting a grocery item" do
    it "removes the item" do
      within ".grocery-item-#{banana.id}" do
        click_on "Delete"
      end

      expect(page).not_to have_content(banana.name)
    end
  end
end

This test is okay but it’s a little bit noisy. We shouldn’t have to care about the DOM details of how the Delete button for this grocery item is located.

Let’s see if we can improve this code with the help of a Page Object.

RSpec.describe "Grocery list", type: :system do
  let!(:banana) { create(:grocery_item) }
  let!(:grocery_list) { PageObjects::GroceryList.new }

  describe "deleting a grocery item" do
    it "removes the item" do
      grocery_list.click_delete(banana)
      expect(grocery_list).not_to have_item(banana)
    end
  end
end

Here’s the Page Object code that made this cleanup possible:

module PageObjects
  class GroceryList
    include Capybara::DSL

    def click_delete(item)
      within ".grocery-item-#{item.id}" do
        click_on "Delete"
      end
    end

    def has_item?(item)
      page.has_content?(item.name)
    end
  end
end

I find that Page Objects only start to become valuable beyond a certain threshold of complexity. I tend to see how far I can get with a test before I consider bringing a Page Object into the picture. It might be several weeks or months between the time I first write a test and the time I modify it to use a Page Object. But in the cases where a Page Object is useful, I find them to be a huge help in adding clarity to my tests.

One thought on “How to clear up obscure Rails tests using Page Objects

  1. Máximo Mussini

    I really enjoyed your post Jason, it reminded me of this talk about test robots:

    https://jakewharton.com/testing-robots/

    I’ve recently open sourced a library that provides page objects as the one you have illustrated in the example.

    https://github.com/ElMassimo/capybara_test_helpers

    Would love to hear your thoughts about it if you try it out 😃

    Here’s an example using the case you analyzed above:

    https://gist.github.com/ElMassimo/53c78faa935654709f4dd9f3de45fb0b

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *