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:

A somewhat obscure test A second, cleaner version of the same test, using a Page Object 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

require 'rails_helper' feature 'Update OCT data', js: true do before do create(:appointment_module_type, name: 'OCT') appointment = create('schedule/appointment') login_as(appointment.physician.user) visit patient_chart_path(appointment.patient) end scenario 'valid inputs' do click_on 'Add' click_on 'OCT' click_on 'OCT' fill_in 'Notes', with: 'OCT data' click_on 'Save' click_on 'OCT' fill_in 'Notes', with: 'other OCT data' click_on 'Save' click_on 'OCT' expect(page).to have_field('Notes', with: 'other OCT data') end end

I find this test pretty hard to follow, and I’m the one who wrote the code. Why are we doing click_on 'OCT' twice in a row? Is that necessary or is that a mistake? (After I re-familiarized myself with the UI that this test exercises, I found that click_on 'OCT' is in fact necessary. The first click_on 'OCT' selects OCT from a menu of options, and as a result a new OCT button is added to the page. The second click_on 'OCT' clicks the new OCT button.)

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

require 'rails_helper' feature 'Update OCT data', js: true do before do create(:appointment_module_type, name: 'OCT') appointment = create('schedule/appointment') login_as(appointment.physician.user) visit patient_chart_path(appointment.patient) end scenario 'valid inputs' do chart_appointment = ChartAppointment.new.add_module('OCT') .save_module_data('OCT', notes: 'OCT data') .save_module_data('OCT', notes: 'other OCT data') expect(chart_appointment).to have_module_data('OCT', notes: 'other OCT data') end end

I dare say that that’s much better. Even without understanding the domain-specific terms in this test, you can see now that we’re 1) adding a module to something called a “chart appointment”, then we’re 2) saving some module data, 3) saving some different module data, then finally 4) expecting that the most recent module data is what actually got persisted.

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

class ChartAppointment include Capybara::DSL def add_module(module_name, sign: nil) find('.appointment-module-add-button').click page.driver.browser.switch_to.alert.accept if sign click_on module_name self end def save_module_data(module_name, options = {}) click_on module_name fill_in 'Notes', with: options[:notes] click_on 'Save' self end def has_module_data?(module_name, options = {}) click_on module_name has_field?('Notes', with: options[:notes]) 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.