Optimizing Page Factory

While you should know by now that you should not be using the Selenium PageFactory class, it is by far the most common Page Object implementation in Java. If you are starting from scratch there are much better solutions; but if you are using PageFactory and you want to improve it without rewriting everything, here is a simple way to get better performance without sacrificing reliability.


To start with, let me reiterate, you should not be using PageFactory. Simon Stewart himself said that he wrote it to be an example of what you could do with Selenium, not so that it would be seen as the official means of doing page objects: “Don’t use PageFactory… It was a terrible mistake putting it [in Selenium], right? Because now I can’t take it out of the project.”

Out of all of the limitations and suboptimal choices PageFactory forces on users, I’m going to just focus on one, which is the efficiency of the code’s communication with the driver. I spend a lot of time showing Sauce Labs users how to boost performance on our service by minimizing unnecessary commands.

Page Factory does not, by default, store the result when the driver locates the element. That means that for each thing you do with the element, PageFactory relocates it first. The ExpectedConditions.elementToBeClickable() method is a good example of this. Using this method, then clicking the element, will send the following commands:

  1. Locate Element
  2. Check if Element Displayed
  3. Locate Element
  4. Check if Element Enabled
  5. Locate Element
  6. Click Element

To make matters worse, more often than not, when I look at people’s code, they are calling these methods multiple times in a row. So these 6 commands can end up being 10 commands, or sometimes 20 or 30 commands.

Most of the time, only two commands are needed for this example, but let’s focus on the fact that we’re locating the element three times instead of just once. I’ll show examples of a more aggressive approach in a future post.

PageFactory does provide an annotation for caching elements: @CacheLookup. This may seem like a great idea, but before you go and add it to each of your element definitions, beware of its primary drawback, the dreaded StaleElementException. Chromedriver references an element by its location in the Document Object Model (DOM). If the DOM is reloaded by refreshing the page, or it changes in such a way that the element moves positions within the DOM, Selenium will consider that element stale. I’ve previously discussed how Watir and Selenium differ in their approaches to defining an element.

When you relocate an element before each interaction, it is much less likely to go stale. It is still possible that the element will go stale in the brief time between commands, but you can ensure that your tests remain robust by catching the error and relocating it.

The danger of @CacheLookup is that once the element is located, you are stuck with it. If that element goes stale, you can’t catch the exception and try again. It’s better to have a reliable test suite and send extra commands than to use the @CacheLookup annotation.

There is another way to cache your elements, though. Page Factory elements were implemented with a Proxy that passes the methods to the RemoteWebElement class. So while you treat it like a WebElement object, it isn’t actually a WebElement. That means we can cast the object to WrapsElement and actually get the underlying Selenium element with: ((WrapsElement) pageFactoryElement).getWrappedElement();

An example of using WrapsElement to click an element:

WebElement cachedElement = ((WrapsElement) pageFactoryElement).getWrappedElement();

new WebDriverWait(driver, 20).until(ExpectedConditions.elementToBeClickable(cachedElement));

cachedElement.click();

Keep in mind that even if the element has been previously located, calling getWrappedElement() will always make a findElement() call to the driver, so you should only use it once and store the result for all future usages. This is easiest to do by wrapping the WebElement instance, and I’ll cover how to do that in a future post.


Follow me if you found this article interesting,
or answer one of these questions in the comments or on Twitter: