Test Automation Framework (Selenium with Java) — Daddy Issues or Page Factory and Elements Related Exceptions

S01E05 of the Test Automation Framework series about everything you’ll need to set up the nice, simple, yet sophisticated framework.

Covered with clear explanations and pretty illustrations.

Sounds like fun? Cool. Now, please, fasten your seatbelts because you’re here for a ride.

S01E01 — What To Automate?

S01E02 — Test Automation Environment and Tools

S01E03 — The First Selenium Test Case

S01E04 — Selenium Foundations Revisited

S01E06 — Page Loading Strategies and Waits

S01E07 — Translating JIRA with Selenide (with Exercises)

S01E08 — JIRA, Selenide, Complex SQL, Java Objects with Equals & HashCode (with Exercises)

S01E09 — Code Review and Refactoring (Part 1)

S01E10 — Code Review and Refactoring (Part 2)

S01E11 — Allure in Action

After I posted the fourth episode of Test Automation Framework on the r/QualityAssurance subreddit, user romulusnr has raised a concern:

Oh. You’re using PageFactory. Oh dear. I’m sorry. I’m so, so sorry.

Which got me thinking: “what’s this about?”. After a while, I’ve found the StackOverflow thread where people were talking about Selenium’s implementation of the PageFactory.

JeffC wrote:

Here’s Simon Stewart, Selenium project lead and creator of the Page Factory, at the 2017 SeleniumConf in Austin. During his keynote address he says not to use Page Factory. This section of the talk starts here:

https://youtu.be/gyfUpOysIF8?t=1517

Actual statement is at 27:25.

I watched the video, and I recommend it, but there wasn’t anything regarding why using the PageFactory is a bad idea. I mean, like a real-world example.

Therefore, I’ve made my research with my good ol’ friend — IntelliJ IDEA’s debugger.

Page Factory Class — Absurdly Detailed Overview

Source code I used at the beginning of writing this guide is available at: https://github.com/n4bik/test-automation-framework/tree/SteppingStones

Here are steps I made to understand the PageFactory:

1 — I’ve created a new HomePage class in the pl.tomaszbuga.pom package. Inside the HomePage.java I’ve defined two different WebElements and used the @FindBy annotation to set the Locators. The yellowButton is WebElement from the homepage of the Ultimate Stack Developer application. The blueButton, on the other hand, is just a mock I’ve made to understand how the PageFactory.proxyFields() method is working when there are multiple fields present. The final class code snippet is presented below.

2 — I’ve changed the FirstTest.firstMethod() so that it creates a new Page Object Model. The final class code snippet is presented below.

3 — I’ve set a breakpoint on the PageFactory.initElements() method within the BasePage.java and started my debugging journey.

Below you can learn more about my findings. If you want a simpler version of it — scroll down for a less detailed explanation.

This is a result of a couple of hours digging into Selenium intestines with a debugger.
Debugging the PageFactory. yellowButton’s Proxy (Locator) has been already assigned. blueButton is null, as the breakpoint is set at line 53 which is a Reflection-based Proxy assignment.

Page Factory Class — Human-Friendly Overview

This is the visual representation of the answer provided by StackOverflow’s user ralph.mayr — link to the thread can be found in the Sources at the bottom of this article.

Okay, but “what’s wrong with that?”, one might ask.

If you’re testing a static page — there is nothing to worry about and you can proceed with another article.

However, in case you’re testing Single Page Applications or any dynamic page so to speak, then you may (and most probably — will) encounter the StaleElementException.

Let’s talk about that, shall we?

Examples and StaleElementExecption proofing which I’m going to present here are based on the article from javastart.pl, written by Mateusz Ciołek: https://javastart.pl/baza-wiedzy/testy-automatyczne-selenium/lokator-z-adnotacji-find-by

StaleElementException example using the search field on the Selenium homepage. The search field is still available, but the Document Object Model has been changed.

What you can see on the animation above is a simple example of when the StaleElementException will appear.

Why? Because you make some action on WebElement on the page that invokes DOM change and then you try to reuse the same WebElement to perform some other action. That’s a no-no because the Proxy object we’ve mentioned in the previous section is now stale (it has moved within the DOM’s tree structure), hence — StaleElementException.

Here’s a code snippet with the exact situation like the one described above. You can validate it yourself, by adding this code to the HomePage.java and executing the homePage.seleniumStaleElementTest() method within the FirstTest.firstMethod().

Okay, so how to prevent that from happening and use the PageFactory with FindBy annotations?

We could’ve refreshed the variable as shown on the code snippet below. However, this would’ve been counterproductive, messy, and we would violate the DRY principle, as we would run the same code twice.

Okay, let’s cut to the chase.

I’m not going to list down every possibility that we could’ve done to prevent that StaleElementException, though. I’d like to show you how it has been done in the article by Mateusz Ciołek, cause I find the presented solution very effective. Nevertheless, I had to make some changes to the proposed solution, as the code wasn’t working as expected.

Here you can find the source code for the Selenium training by javastart.pl, which elements I’m going to use as a template for our solution: https://github.com/ilusi0npl/selenium-kurs-temat-9/tree/topic/chapter31

1 — Let’s make a new, custom-tailored WaitForElement utility class. To do that, I’ll create a package titled pl.tomaszbuga.utils

2 — Within utils create a new WaitForElement.java file

3 — Copy the code below and paste it within the WaitForElement.java

4 — Create ByLocatorFromString.java in the utils package. This class will be used when passed WebElement would be an instance of RemoteWebElement.

What is the RemoteWebElement? Basically, it is a WebElement found by the WebDriver.findElement() method (instead of the FindBy annotation, which generates a Proxy object).

Remember:

FindBy annotation creates the Proxy object and DOES NOT initiate lookup for the WebElement by WebDriver

WebDriver.findElement() method creates the RemoteWebElement and DOES initiate lookup for the WebElement by WebDriver

5 — Copy the code snippet and paste it within your ByLocatorFromString.java

6 — Now, create a ByLocatorFinder.java in the utils package

7 — We’re going to use the Java Reflection to retrieve the By locator values. And to make this process easy, we’re going to use Apache’s Commons Lang3 package. Therefore — please update your pom.xml file accordingly.

8 — Copy the code from the snippet below and paste it to ByLocatorFinder.java

package pl.tomaszbuga.utils;

import org.apache.commons.lang3.reflect.FieldUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.pagefactory.DefaultElementLocator;

import java.lang.reflect.Proxy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static pl.tomaszbuga.utils.ByLocatorFromString.getByLocatorFromString;

public class ByLocatorFinder {
private static final String BY = "by";
private static final String H = "h";
private static final String LOCATOR = "locator";
private static final String FOUND_BY = "foundBy";

public By getByFromWebElement(WebElement element) {
try {
if (element instanceof DefaultElementLocator) {
return getByLocator(element);

} else if (element instanceof Proxy) {
Object proxyOrigin = getField(element, H);
Object locator = getField(proxyOrigin, LOCATOR);

return getByLocator(locator);

} else /* if WebElement is RemoteWebElement */ {
String foundByString = getFoundBy(element);
String foundByPattern = "(?<=\\-> ).*";

Pattern pattern = Pattern.compile(foundByPattern);
Matcher matcher = pattern.matcher(foundByString);

if (matcher.find()) {
int locatorDefinitionIndex = 0;
String locatorDefinition = matcher.group(locatorDefinitionIndex);

return getByLocatorFromString(locatorDefinition);

} else {
throw new IllegalStateException("Failed to get locator from RemoteWebElement. Please, check if the Regex pattern is valid.");
}

}
} catch (IllegalAccessException e) {
throw new IllegalStateException("Failed to get locator from WebElement, due to: ", e);
}
}

private Object getField(Object element, String fieldName) throws IllegalAccessException {
return FieldUtils.readField(element, fieldName, true);
}

private String getFoundBy(Object element) throws IllegalAccessException {
return (String) FieldUtils.readField(element, FOUND_BY, true);
}

private By getByLocator(Object element) throws IllegalAccessException {
return (By) FieldUtils.readField(element, BY, true);
}
}

Now, you’re probably wondering what are those if-else statements responsible for. I know I did. Let me show you the structure of the Proxy object and the RemoteWebElement so that you can understand what’s going on there at the moment when ByLocatorFinder.getByFromWebElement() is launched.

This is a structure of the Proxy object when it arrives at the if-else statement within the ByLocatorFinder.getByFromWebElement() method.
This is a structure of the RemoteWebElement when it arrives at the if-else statement within the ByLocatorFinder.getByFromWebElement() method. The String is then passed to the getByLocatorFromString() method to create a new By locator out of the String value.

9— Update the WaitForElement.java code. I’ve added the ByLocatorFinder.getByFromWebElement() and now we’re using the ExpectedConditions.visibilityOfElementLocated() instead of .visibilityOf(), because it’s using the By locator to make sure that WebElement is visible. We don’t need to change the .elementToBeClickable(), because by default it’s overloaded method and it works fine with both WebElement and By objects.

10 — Update the HomePage.java code and give it a spin!

Finally — it’s working like expected.

Summary of the StaleElementException proofing

I know — this topic is tough, no doubt about that.

I’ve spent over two days of researching, and debugging just to write this article. However, I strongly recommend learning how it’s working (I really hope this article can help you with it), if you’re aspiring to get better at writing automated test cases using Selenium and Java. Because it can be one of the questions that you’ll encounter on the job interview (e.g., “How does the PageFactory works?” or “How would you overcome the StaleElementException?”.)

  • We’ve learned that Reddit is a great place to share your articles and get some insight from other users
  • We’ve learned how the PageFactory.initElements() works
  • We’ve learned what is StaleElementException and how to avoid them with the custom WebDriverWait & ByLocatorFinder classes

In case of any questions (I believe there can be one or two of those) — feel free to post them in the comments section below.

All the best,

Tomasz Buga, SDET

www.tomaszbuga.pl

Sources:

GitHub Repository available at: https://github.com/n4bik/test-automation-framework/tree/DaddyIssues

All illustrations made by Tomasz Buga

--

--

Tomasz Buga

Tomasz Buga

Software Development Engineer in Tests. Passionate about programming. Experienced, former employee of the insurance industry. Graphic designer by choice.