Saturday, August 4, 2012

The Page Object pattern for UI tests

Do you do web/UI functional tests in your team? You know, the ones where Selenium, for instance, fires up Firefox, IE, or Chrome, and navigates through the app for all the world like a player piano in an empty room. If so, you want to use the Page Object model.

From the Selenium wiki: "The Page Object pattern represents the screens of your web app as a series of objects." If you're not already conforming to it, you will eventually stumble upon this pattern. So you may as well stride purposefully towards it. Ultimately, the best way to see the benefits of using it is to do UI tests using the Stupid pattern first, which we did in my team. That's the pattern where you have hard-coded strings, button ids, and ad-hoc ways of getting from one screen to another right in your test methods. About as DRY as Sea World. Test code should be treated like regular code, and you wouldn't ordinarily write your app's code in such a procedural, slapdash way.

I can proudly stand up and say that all of the pages in my current project are represented as Page Objects. Every single one. All right then, they aren't - but they're in the process of being converted to being Page Objects as I write this.

So - what's a Page Object? In our case, it's an abstract class containing (among other things) a constructor:
protected PageObject(SeleniumBase browser)
    if (browser == null)
        throw new ArgumentNullException("browser");
    Browser = browser;
with some methods like this (using the actual model classes from our main app):
public DestinationPage SubmitTheForm(FormModel model)
    // boilerplate Selenium code to find form elements, fill them with model data, and submit the form
    return NavigateTo<DestinationPage>(By.Classname(".submitButton"));
made possible by this beautiful generic base class method:
protected TDestinationPage NavigateTo(By clickDestination)
where TDestinationPage : PageObject
    return (TDestinationPage) Activator.CreateInstance(typeof (TDestinationPage), Browser);
[Photo of a water mill]

This method is the ball bearing, the engine, nay - the water mill of our functional tests suite. It keeps things moving. It does the important work of clicking a link or a button, or some sort of page navigation element (Navigate()), then dutifully completes the corresponding paperwork by newing up (Activator.CreateInstance()) an instance of the type passed in to it, so you can chain methods in a meaningful expression like:
HelpPage page =


As you can see, this NavigateTo method allows for method-chaining. This "fluent" approach reminds me of Linq expressions or jQuery methods which are the ways I became familiar with chaining. "Methods on the PageObject should return other Page Objects" says the Selenium docs. I also find it useful to have other properties on the Page Object simply returning strings that I expect to find on the page to prove the state of the page, for instance that there is a particular message showing. But generally speaking, Page Object methods return Page Objects.

I was speaking to Bradley@Readify about all this chaining, and he cautioned against violating the Law of Demeter (or as Martin Fowler calls it, "the Occasionally Useful Suggestion of Demeter") in situations like this. I think though as developers we're conditioned to see all those dots as Demeter territory, whereas in this case there's no actual composition denoted by the dots. Of course, Brad knew this. He was just sayin'.

A fork in the road

Sometimes a Page Object method may result in two different paths being taken, depending on an input parameter, for instance an answer which may be right or wrong. In the Selenium wiki they suggest explicitly creating methods to handle this approach, but I prefer to use generic methods. I'll show you what I mean. Starting with this test:
this.Given(_ => GivenMyPasswordIsReset())
    .When(_ => WhenIChangePassword(_resetPassword), "When I use my old password")
    .Then(_ => ThenIShouldBePromptedToChangeMyPassword())
    .And(_ => AndIShouldSeeAWarningAboutUsingTheSamePassword())
I call this helper method within the test itself (because I have another test where I do more or less the same thing except that I use a new password):
void WhenIChangePasswordTo(string password) where TPage : Page
    HomePage.GoToLogonPage().LogonAndChangePassword(_user, password);
which in turn calls this, in the LogonPage Page Object:
public TPage LogonAndChangePassword(User user, string newPassword) where TPage : Page
    return LogonAndChangePassword(user).ChangePassword(user, newPassword);
This can result in the user being successfully directed to the home page, or the Change Password page, depending on whether they use the old or new password. I don't see that there's any difference in terms of the responsibilities of the cleint test class and the Page Object. Sure the test class dictates what page it expects to be returned to, by means of the type (e.g. ChangePasswordPage) that it passes the Page Object, but in the case sugested by the Selenium wiki, it has to explicitly call a method called LogonAndChangePasswordWithOldPassword(), or such like. Generics win, I think.


When we invoke a method on the Page Object like Logon(), we're chunking - treating a bunch of things at one level as if they were one thing on a higher level. On one level you might have instructions like "fill in the username field, fill in the password field, and press submit". On a higher level you would have "log on". We can ignore the internal structure of the lower level operation. In the same way, you can have a chunked operation of chunked operations on the Page Object.

Say you have a path that's commonly taken through your app, for example one where a user goes to the home page, logs on, and checks their messages, then you might want to chunk those operations into one aggregate one for convenience. That chunking should obviously happen on the test class itself, because it doesn't make much sense to have a MessagesPage object have to concern itself with the details, however high-level they may be, of logging on.
_messagesPage = HomePage.GoToLogonPage().Logon(user).GoToMessagesPage();
can become
_messagesPage = CheckMessages();
as long as CheckMessages() returns a MessagesPage object. This is all rather obvious I'm sure, but it's interesting to think about the movement up and down the levels you have to do when composing a test with a Page Object.

Also, even though you can new up a new MessagesPage() in each of these cases instead of reusing _messagesPage in consecutive methods, it goes against the spirit of the test which is to simulate moving through the app by getting from one page to another. Unless you're saving state on the page as you go, I guess it doesn't really matter, but reusing the variable is a truer representation of what's happening in the browser: the user gets to a page, then clicks on a link in that page to go to another one - it's the same page they entered and exited.

The wrap-up

The Page Object pattern allows us to do two things mainly:

1) Encapsulate the low-level "internals" of the page - the ids of the buttons, classes of elements, etc. that we want to click on, find, assert are visible, do anything with. Page Objects "should seldom expose the underlying WebDriver instance."
2) And generally, make the client (of the Page Object) test methods more elegant by taking them to a higher level.

Thinking in higher levels is what distinguishes chess masters from chess chumps like me. I'm crap at chess, I should point out. Couldn't beat Ricey, couldn't beat Nagy. But it remains fascinating to me. Grandmasters don't analyze the game by thinking ahead more moves than you or I, but rather by seeing the game at a higher level than you or I would.
Intelligence depends crucially on the ability to create high-level descriptions of complex arrays, such as chess boards, television screens, printed pages, or paintings.
from "Gödel, Escher, Bach: an Eternal Golden Braid" by Douglas Hofstadter

1 comment:

  1. I would like to know if you can post a sample of a project with the page object pattern ?