Beyond Page Objects: liberate yourself from the chains of UI-think!
John Ferguson Smart
I Help Manual Testers Become World-Class Test Automation Engineers. Agile Test Automation and BDD Expert | International Speaker And Author | Coach, Trainer and Mentor
So you have Page Objects in your test automation suite? That’s great! But it’s not enough! Page Objects are a great start, but you need to go further if you want truly sustainable, high quality test automation.
The Page Objects origin story
Page Objects are a popular automated web testing pattern first implemented for Selenium by Simon Stewart himself Simon Stewart back in 2009. The idea is to model web pages or UI components as objects. You reuse these objects in different tests to avoid duplication and simplify maintenance.
The idea is to model web pages or UI components as objects. You can then reuse these objects in different tests, avoiding duplication.
The ideas behind Page Objects is a good one. Keeping the WebDriver selectors for a given page in one place makes it easier to keep the test suites up to date when the page changes. They give you a layer of abstraction between your test logic and the elements on the page you want to manipulate. This way, you only need to worry about which element you want to use, not how to locate it.
The problem with Page Objects
The problem is, Page Objects are only the first layer of abstraction. They were only ever intended as a first step to guide inexperienced testers away from the imperative scripting style of automation that was prevalent in the late 2000s.
But if you want to keep your tests maintainable and robust, Page Objects are just the bare minimum to get you started. You can do much better. Page Objects are like training wheels. They help you get started, but eventually you should outgrow them.
Too much how, not enough what and why
When you write a test built around page objects, you think in terms of the UI. You think in terms of how the user interacts with a page. The user enters a value into a field, the user clicks on a button, and so on.
But well-written tests don’t simply mimic the user’s every action, they describe the user journey through the system. They describe what the user is doing, and why.
For example, if we were testing a “todo” list application, a basic test about marking an item in a todo list as complete might look like this:
@Test
public void should_be_able_to_complete_a_todo_with_steps() {
driver.get("https://todomvc.com);
getDriver().findElement(By.id("new-todo"))
.sendKeys()("Put out the garbage", Keys.ENTER);
getDriver().findElement(By.id("new-todo"))
.sendKeys()("Walk the dog", Keys.ENTER);
getDriver().findElement(By.xpath("//div[@class='view' and contains(.,'Walk the dog')]"))
.findElement(By.xpath(".//input[@ng-model='todo.completed']"))
.click();
assertThat(getElement(By.xpath(""https://*[@class='view' and contains(.,'Walk the dog')]//input[@type='checkbox']").isSelected(), is(true));
}
This code works, but notice how hard this code is to read? You need to work very hard to figure out what the selectors are doing. And this makes it harder to understand what feature the test is actually demonstrating. There is also a lot of duplication. Both of these factors make this kind of test code extremely hard to maintain.
Using Page Objects, the code might look more like this:
TodoListPage todoListPage;
@Test
public void should_be_able_to_complete_a_todo_with_steps() {
todoListPage.open();
todoListPage.getTodoField().type("Put out the garbage", Keys.ENTER);
todoListPage.getTodoField().type("Walk the dog", Keys.ENTER);
todoListPage.getCheckboxInRow("Walk the dog").click();
assertThat(todoListPage.getStatusInRow("Walk the dog"),
is("Completed"));
}
Here, the page object has hidden the selector logic, which will make the code easier to maintain. But we are still reasoning in terms of typing and clicking, which makes it hard to see at a glance what the test is doing. We are still talking about how we perform an action, not what action we are performing, and this still makes our tests harder to read.
Overweight Page Objects
Using more high-level Page Objects, our test might look like this:
TodoListPage todoListPage;
@Test
public void should_be_able_to_complete_a_todo_with_steps() {
todoListPage.openApplication();
todoListPage.addATodoItemCalled("Put out the garbage");
todoListPage.addATodoItemCalled("Walk the dog");
todoListPage.markAsComplete("Walk the dog");
assertThat(todoListPage.statusOf("Walk the dog"), is(Completed));
}
This code is easier to read, since we are reasoning more in terms of business actions like “add a todo item” and “mark as complete”, rather than simply performing UI interactions. And our test is still tightly bound to the UI.
Higher level Page Objects also tend to become bloated, as more and more business logic creeps in alongside the logic for locating the page elements. Which in turn makes them harder to maintain.
When all you have is a Page Object everything looks like a UI test
But there is a bigger problem with using Page Objects as the foundation of your test automation strategy. When we have a library of Page Objects like this, we will naturally tend to implement all of our tests using these Page Objects. Our tests end up modelling the way the user interface works, rather than what the user is doing, and what outcomes the user wants to achieve.
Beyond Page Objects
Now imagine a test that models not pages, but actual business tasks. Imagine a test that described what the user was trying to do in business terms, rather than what buttons she clicks on and what fields she enters.
The Screenplay pattern is one way to do this. In Serenity Screenplay in Java, for example, we could write something like this:
public void should_be_able_to_complete_a_todo() {
givenThat(james).wasAbleTo(
Start.withATodoListContaining("Walk the dog",
"Put out the garbage"));
when(james).attemptsTo(
CompleteItem.called("Walk the dog")
);
then(james).should(
seeThat(TheItemStatus.forTheItemCalled("Walk the dog"),
is(Completed))
);
}
In addition to being very readable, this code is much more declarative than the previous examples. We are no longer thinking about what a particular page does, or how the user interacts with a page. These details are hidden away. Rather, we are thinking about the user’s business activities and goals. We do this by reusing objects that represent actual business tasks and business concepts, such as CompleteAnItem and TheItemStatus.
But there is more. Since we are no longer thinking in terms of the user interface, of pages, buttons and input fields, we can allow a lot more flexibility as to how each step or task is implemented. For example, we could now imagine setting up the todo list via a REST API and not going through the screens. And this would speed up our test suite immensely:
givenThat(james).wasAbleTo( Start.withATodoListContaining("Walk the dog", "Put out the garbage") .viaTheRestAPI() );
Conclusion
If you are moving from imperative script-style testing to a Page Object model, congratulations! You are on the right track. But don’t stop there. If you want your test suites to be truly maintainable and scaleable, model what the user is doing in business terms, rather than howthey are interacting with the UI. And this will free you of the urge to test everything through the UI, and open up the possibility of testing your application in more interesting ways.
Related Reading
- Page Objects Refactored – SOLID steps to the Screenplay/Journey Pattern
- User-Centred Design: How a 50 year old technique became the key to scalable test automation
- Better Automated Acceptance Tests with Serenity Screenplay – the Screenplay pattern in Java with Serenity BDD
- From Scripts to Serenity – implementing the Screenplay pattern in JavaScript
Related courses and workshops
- Serenity and Screenplay: Next-generation Automated Acceptance Tests – Learn about Screenplay and Serenity BDD in this new online course.
- Advanced BDD Test Automation – Or get us to come onsite and learn how to write more robust, higher quality automated acceptance tests using state of the art test automation practices (in Java, Javascript or .NET)
I Help Manual Testers Become World-Class Test Automation Engineers. Agile Test Automation and BDD Expert | International Speaker And Author | Coach, Trainer and Mentor
7 年Yes, the automation testers do need to be familiar with the builder pattern and other basic SOLID concepts of OO development. Fortunately this generally isn't too hard to learn.
Testability advocate | co-founder Testingscool | Humans in Learning | Romanian Testing Conference
7 年I also favour the approach, is more human readable and our BAs appreciate the effort. However it requires special attention not to get lost in return type of your methods
Directeur at TechChamps | TSC Member and Core Contributor to the WebdriverIO Project
7 年Completely agree. Personally I make a distinction between a 'component object' and a 'page object'. The 'component object' will hold the locator, and for an input a 'get/setValue' and so on. A 'page object' can hold multiple 'component objects', for example your form. Finally you will get your 'task object' file that will hold the 'tasks / behaviour'.