The Journey pattern - the successor to PageObject
“The Journey pattern” supplants PageObject as the go-to UI automation pattern. This post explains why you should care and how you can start upgrading your PageObjects tomorrow.
The home favourite: the PageObject pattern #
The Page Object pattern has become the de-facto UI automation pattern - even the official Selenium docs include a description. The idea is to separate your test code into classes corresponding to each different screen (i.e. ‘Page’), with one method for each interaction with the page. To simplify the example from the docs, the following code
driver.findElement(By.cssSelector("#username")).sendKeys("myUsername");
driver.findElement(By.cssSelector("#password")).sendKeys("myPassword");
driver.findElement(By.cssSelector("#loginButton")).click();
becomes
new LoginPage(driver).loginAs("myUsername", "myPassword");
// PageObject
public class LoginPage {
private WebDriver driver;
private By usernameTextbox = By.cssSelector("#username");
private By passwordTextbox = By.cssSelector("#password");
private By loginButton = By.cssSelector("#loginButton");
public LoginPage(WebDriver driver) {
this.driver = driver;
}
public void loginAs(string username, string password) {
driver.findElement(usernameTextbox).sendKeys(username);
driver.findElement(passwordTextbox).sendKeys(password);
driver.findElement(loginButton).click();
}
}
This approach has some key maintainability advantages over the original:
- DRY - Don’t Repeat Yourself. Avoid repeating the same information in more than one place - as this makes it easier to update when it changes. For example, many of your tests will start by logging in to the application. If we just copy/pasted the first example into every test, then if the CSS selector for the username textbox changes, we’d have to change it in every test. If instead every test accesses it via the LoginPage class, we only have to change the selector in a single place. It’s usually best to assume that everything will change eventually.
- Abstraction. We have moved from describing CSS selectors and keyboard/mouse actions to describing intent. The top example is full of details we need to consider together to work out what the test is trying to do, whereas in the second the details are ‘abstracted away’, replaced with an effortless statement of what we want to achieve. This also helps us fix code when it’s broken: the method name tells us what the intent of the code is - so if it does something else we know it’s broken.
- Encapsulation. By making the selectors into private fields in the PageObject class, we hide them from external view and ensure they’re only accessible where they’re needed. This simplifies the public interface of the class and makes it easier to use it correctly. If they were publicly available then the next person might (reasonably) expect it was ok to refer to them directly from their test. In general your life will be easier if a class exposes either information or operations - but not both.
Chinks in the armour #
However it’s not all unicorns and rainbows: these benefits aren’t free, and this is what we traded-off for them:
- Total code length has increased. Our second example is substantially longer. If our class is used many times, we may eventually save more lines than we added - but for less common actions we may just be ballooning our codebase. As a rule of thumb the rate of defects/loc (line of code) is approximately constant - so the more code you write, the more bugs you’ll have, and the longer it’ll take newcomers to get up to speed.
- Over-encapsulation. Encapsulation (and software design in general) can be great for reducing the cost of anticipated changes but often increases the cost of unanticipated changes. For example, if we want to add new test functionality that re-uses the username and password selectors, we can’t do it without first modifying the PageObject, as the selectors aren’t ‘visible’ outside of it. This makes it harder to try things out quickly and increases the costs of adding new tests.
And we also left some problems unsolved:
- Re-use between pages. Test code for functionality that occurs on many pages (e.g. a search box), shouldn’t need to be duplicated on every PageObject that needs it. Mix-ins and traits can be used to great effect, but in languages that don’t support them single-inheritance creates rigid hierarchies which cause more problems than they solve. If PageX needs test functionality A, B and C, what order do they all have to inherit each other - and if it loses functionality C later, how does the order have to change? Time spent debating these questions is time not spent adding tests.
- Separation of concerns. We separated our intent from our implementation, which is a great start - but our PageObject class still contains information about how to find elements on a page, what actions we can perform on them, and what business tasks we’re trying to accomplish with them.
The contender: the Journey pattern #
In my modified version of the pattern (which loses some concepts from the original), test code is separated into a number of different ‘concerns’ for increased maintainability:
// Goal - to verify login works (assertions omitted)
LogIn.as(driver, "myUsername", "myPassword");
// Task:
public class LogIn {
public static void as(WebDriver driver, string username, string password) {
Enter.textInto(driver, username, LoginScreen.usernameBox);
Enter.textInto(driver, password, LoginScreen.passwordBox);
Click.on(driver, LoginScreen.loginButton);
}
}
// Screen:
public class LoginScreen {
public static By usernameBox = By.cssSelector("#username");
public static By passwordBox = By.cssSelector("#password");
public static By loginButton = By.cssSelector("#loginButton");
}
// Actions:
public class Enter {
public static void textInto(WebDriver driver, string text, By selector) {
driver.findElement(selector).sendKeys(text);
}
}
public class Click {
public static void on(WebDriver driver, By selector) {
driver.findElement(selector).click();
}
}
- Goals are test cases, expressed as a sequence of tasks and assertions. As usual, the name of the test case should express the functionality we want to verify.
- Tasks are units of ‘intent’, composing one or more actions (or other tasks) with the elements of one or more screens. They sit at a mid-level of abstraction between goals and actions.
- Screens are the closest we get to the old ‘PageObjects’ - but all the methods are gone, replaced with a set of public fields which locate the various UI elements our tests need to access. Elements which are common to multiple screens can be grouped into a ‘SearchScreen’ or ‘LogoutScreen’ without any need for inheritance.
- Actions do all of the direct interaction with the Selenium API, and allow us to pick a more readable title for what we’re doing - accessing elements, sending text, clicking on buttons etc. No screen or action should reference another screen or action. Edit: if you feel a particular action is unnecessary as it provides so little value over a direct call to the Selenium API, feel free to just do that. It can be really helpful for actions that don’t map exactly to a single webdriver method - the examples above are not the strongest.
A sharper edge #
- Single responsibility principle. The goal of separation of concerns is to segment functionality between classes until each class has only one ‘responsibility’, or ‘reason to change’ - and we seem to have done pretty well. The details of the UI, the Selenium API calls we want to make, the functionality we want to test and the operations that comprise it are cleanly separated into the screen, action, goal and task classes, respectively.
- Smaller classes. Whereas PageObjects could very quickly grow for pages that need to perform 10s of different operations, screens can be easily divided up into different groups as necessary without any re-work, and no task or action has any good reason to grow in size, so all should stay manageable, and at-a-glance readable.
- Unencapsulated. There isn’t a single private modifier in the example above, which means the design should never get in your way. Want to try something quick? Call some actions, tasks and screens directly from the test case; pull that code out into it’s own action or task class once it does what you need.
Youthful inexperience #
- Total code length has increased a little more. Our classes may be smaller but we now need far more of them - a PageObject with 10 methods is now a short screen class with 10 accompanying task classes. Each interface is stripped down to the essentials - but there are a lot more of them to choose from.
- Increased time writing new kinds of tests. When we need to do something we haven’t done before, it will take longer now to write the additional task and action classes than it did to just stick another method on a PageObject.
A new champion #
The Journey pattern helps separate out the concerns of UI testing much better than pure PageObjects, so scales much better. It also lends itself well to an incremental transition from PageObjects - one-by-one extracting tasks and actions until all you’re left with are screens!
So have I convinced you - will you be re-writing your Page Objects tomorrow? Are there any other patterns you’ve seen that scale well for UI automation?