Selenium Page Objects : 1 of 2

I have been reading a wayback machine page by Adam Goucher describing how to use Selenium page objects in Python. It's the best proper solution of a problem a lot of bloggers have glossed over and presented a partial pattern for. Even the MAN page for the Python/Java pagefactory module is pants, and glosses over the #1 issue, you want maintainable, debug-able test code that tells you useful things about the app under test. I have found that tool docs often state the obvious anyway, and like so many blog posts about how to use page-object model, nobody is getting REAL.

Enemies

Almost nobody is sharing their good "conventions" tricks, nor the battle with enemies of scale they encountered. Enemies which example blog posts never warn you about. So I'm going to spread this over 2 or 3 posts, but let's cover the core and then get to the deeper chaining and workflow problem in part 2.

Fowler

I have chosen Python here, but this pattern applies in Java just as nicely. The best possible intro was written up by Martin Fowler. https://martinfowler.com/bliki/PageObject.html Fowler even mentions the selenium pattern implementation, with a note on assert style.

One of the biggest problems in any GUI type testing, is that often you need to visit a few different dialogues in an app to set up initial states. It is possible to use back-end API calls to set initial state too, if your context says it's better to do that, then do that. Something however, getting the browser and app to a point where a test is ready to start, becomes a mini-test in itself, or in unit testing parlance, a fixture. But before we go there, lets share my findings after a bit of Googling for better solutions and a good Python starting point.

Goucher

From the Pragmatic Programmer tutorial written by Adam Goucher. :

https://web.archive.org/web/20190428004331/https://pragprog.com/magazines/2010-08/page-objects-in-python

This appears to point to a Python pagefactory module, the post by Adam Goucher seems to have gotten deleted at some point. Hurrah! to the wayback minions. Please read the above link before continuing....
If you have read the above wayback link, lets continue. Here is the example test code the original module author gave:

selenium_unittest.py:

    def login(self):
        # set_text(), click_button() methods are extended methods in PageFactory
        self.edtUserName.set_text("opensourcecms")               # edtUserName become class variable using PageFactory
        self.edtPassword.set_text("opensourcecms")
        self.btnSignIn.click_button()

Adam starts by fixing the bug in the selenium sample code by parameterising the login function in his post. There are a whole raft of tiny issues to be had with the selenium wrapper, mainly around extensibility, so I discarded it very early on.

Syntactic sugar

The first thing we see is that the PageFactory module assumes you initialise a member in your class called "driver" so it can talk to selenium. Fair enough. It also then wants you to add a dictionary of locators, which get automatically turned into class attributes using python __getattr__() . And finally you can add methods to your class as you expect to encapsulate a real world test. This sounds like a recipe that wants a bog standard base class.

I got disappointed by the module not addressing the issue the webElement interface set_text() method that for high-level use and simplicity really needs to implicitly do a clear() before the set_text() for readability. I addressed this by implementing a __settattr__() which does a clear() of the editbox, and send_text() all in one go. As you progress along, you will also find that sometimes you want to drive a element on the page which is not currently in view, the scrolling problem. Most of the time, you want the test to do what a human would do, scroll to the element and then interact with it automatically, inside the set()/get(), so that you don't litter your test code with scrolling code. There are many pitfals in web app automation using selenium that other higher level frameworks try to solve for you. ActionChains sound like the right solution to page scroll issues, but trust me, javascript to scroll to elements is more reliable.

a better login( )

    def login(self, username, password):
        self.email = username
        self.password = password
        self.btn_login.click() 

At a later stage I also started to follow a locator naming convention, and I swapped out "btn_login" in preference of a name-<type> and called it login_button. 

And now out login() has parameterised instead of hard-coded the password here as Adam showed us already. I'm going to go farther than just the convenient login() function, because it deals with a fundamental difficulty with the locators collection and intellisense, the job of writing code without knowing the names of elements. I am going to suggest every form (class in Python) has a submit() method, for consistency, and then go on to suggest that every form also has a populate() function . This defends our tests, whenever the form starts getting busy or even fragile due to constant app under test change. Less repetition of test code that populates the form (DRY principle), improves maintainability and mostly test readability. But what, you might say about negative login tests? Like a blank password?

App conventions

Our login() function may/might raise an exception if the password is empty or the username is empty.  The button normally takes you to a new web page, I'll get to that in part #2, for now let's stay focused on the login problem as an example. This brings up the point about test code that is assertive versus test code that is not assertive. The former tactic makes writing negative test cases easier to do, often at a point in your project where rewriting code has become expensive. The page objects are a "layer", don't let the layer get in your way, only raise exceptions when a contract the object provides would break.

<todo disabled form  button screenshot>

We might be starting to enforce UX rules in our test code, about whether buttons on forms should be disabled until a form is filled out now. Don't bake this kind of UX decision into your pageobject tool, rather enforce it in a helper method if that will help you achieve consistency. At this point you can add code to the login function to also check if an error message get displayed when the username was for example empty or password not meeting a required strength. 

Waits

The other thing I did in my page factory implementation is have the page object constructor wait for the page to load by waiting for all of the locators to be present instead of still requiring you write a WebDriverWait callable first, to handle the page load time problems all in one go.

So the rest of my page object is not really code, and looks like this:

class PageLogin(PageObjectBase):
    """
    The new social login page
    """
    locators = {
        "login_button": (By.XPATH, "//span[contains(.,\'Log in\')]"),
        "email": (By.ID, "email"),
        "password": (By.CSS_SELECTOR, "#password"),
    }

On the topic of locators, I found a small problem space with disabled and hidden controls that only appear later, or are dynamic, in cases where a page change state. Now you could argue for making separate pages when that happens, but it's illogical to do so in terms of mental overhead, code complexity and how to deal with backtracking of state.

<todo: show how we added a list of mandatory locators to deal with hidden ones at page load time>

Here is the code https://github.com/zaphodikus/PageObjectModel , this includes sample html pages, a mini Webserver and a few other tests to play with. 

The real interesting part of this completely standalone test test suite is however https://github.com/zaphodikus/PageObjectModel/blob/main/PageFactory.py 

This implementation copies 2 ideas from other implementations.

  1. element highlighting - a magenta drop-shadow gets added to every element we do a lookup on
  2. animation delay - a settable interval to slow down the automation so you can see what is happening

Enemy of scale

In summary

- easier to maintain code by using page object methods() to encapsulate common tasks like populating or submitting

- page load "wait" using the same locators.

- debugging by highlighting the interactive element

- locator naming conventions

- helper method conventions

- __getattr__ and __setattr__ to make managing the web mechanics easy

Next week in part 2 I'll cover the other parts of this standalone sample, but also the promised solution to simplifying the writing of workflow-like tests that consist of groups of pages that a tester wants to need to execute in a sequence to further remove duplication for common tasks in the web app. I hope this sharing of some code that's actually going in pattern at least, into a live production suite, will help people more than the simple homework answers that are shared in so many blogs.

(Code syntax highlighting done using http://hilite.me/ )

Comments