Top Tips for Selenium Experts - Part 2: Design Patterns
November 13, 2019

Tony Yoshicedo
Parasoft

This blog assumes you have been using Selenium and are comfortable writing test cases, so now you can focus on techniques and design principles to get your UI test automation to the next level. This 2-part blog is broken down into techniques and design patterns. Techniques were covered in Part 1, and design patterns are covered here in Part 2 . I'll be using Java here, but the techniques and practices should be generally applicable.


The Bot Pattern

The bot pattern abstracts Selenium API calls into actions. The actions can then be used throughout your tests to make them more readable and concise.

We have already seen a few examples where this would be useful in this blog. Because it is necessary for an element to be clickable before we try a click it, we may always want to wait for the element to be clickable before each click:

void test() {
/* test code */
WebDriverWait wait = new WebDriverWait(driver, 5);
wait.until(ExpectedConditions.elementToBeClickable(element));
element.click();
 
wait.until(ExpectedConditions.elementToBeClickable(element2));
element2.click();
}

Rather than write the wait condition every time the test clicks an element, the code can be abstracted into its own method:

public class Bot {
public void waitAndClick(WebElement element, long timeout) {
WebDriverWait wait = new WebDriverWait(driver, timeout);
wait.until(ExpectedConditions.elementToBeClickable(element));
element.click();
}
}

Then our code becomes:

void test() {
/* test code */
bot.waitAndClick(element, 5);
bot.waitAndClick(element2, 5);
}

The bot can also be extended to create library-specific implementations. If the application ever begins using a different library, all of the test code can remain the same and only the Bot needs to be updated:

public class Bot {
private WebDriver driver;
private RichEditorBot richEditor;
 
public Bot(WebDriver driver, RichEditorBot richEditor) {
this.driver = driver;
this.richEditor = richEditor;
}
public boolean isEditorDirty() {
richEditor.isEditorDirty();
}
}
 
public class RichEditorBot() {
public boolean isEditorDirty() {
return ((JavascriptExecutor) driver).executeScript("return EDITOR.instances.editor.checkDirty()");
}
}
 
void test() {
/* test code */
bot.isEditorDirty();
}

An example Bot is available as part of the WebDriverExtensions library, as well as a library-specific implementation.

The Bot pattern and the Page Object model can be used together. In your tests, the top-level abstraction is the Page Object representing the functional elements of each component. The Page Objects then contain a Bot to use in the Page Object functions, making their implementations simpler and easier to understand:

public class LoginComponent {
private Bot bot;
 
@FindBy(id = "login")
private WebElement loginButton;
 
public LoginComponent(Bot bot) {
PageFactory.initElements(bot.getDriver(), this);
this.bot = bot;
}
 
public void clickLogin() {
bot.waitAndClick(loginButton, 5);
}
}

WebDriver Factory

Instantiating and configuring a WebDriver instance such as ChromeDriver or FirefoxDriver directly in test code means the test now has two concerns: building a specific WebDriver and testing an application. A WebDriver Factory separates these concerns by moving all WebDriver instantiation and configuration out of the test.

This can be accomplished a number of ways, but the concept is simple: create a factory that provides a fully configured WebDriver. In your test code, get the WebDriver from the factory rather than constructing it directly. Now any concerns regarding the WebDriver can be handled in a single place. Any changes can happen in that one place and every test will get the updated WebDriver.

The Web Driver Factoryproject uses this concept to manage the lifespan of WebDrivers across multiple tests. This complex task is abstracted to the factory allowing tests to just request a WebDriver with the provided options.

The WebDriver factory makes it easy to reuse a single test across multiple browsers. All configuration can be handled through an external file. The test only asks the WebDriver factory for an instance of the WebDriver and the factory handles the details. An example of this is used to support parallel grid testing in the TestNG framework.

Extending the Page Object Model

The Page Object model provides a layer of abstraction that presents the functions of an applications' components while hiding the details of how Selenium interacts with these components. This is a powerful design pattern that makes code reusable and easier to understand. However, there can be a lot of overhead in creating a class for each page and component. There's the boilerplate for each class, then shared components between classes such as initializing the instance and passing around the WebDriver or Bot object. This overhead can be reduced by extending the Page Object model.

If you are using the Bot pattern along with your Page Object model, then each Page Object will need an instance of the Bot. This might look like:

public class LoginComponent {
private Bot bot;
 
@FindBy(id = "login")
private WebElement loginButton;
 
public LoginComponent(Bot bot) {
PageFactory.initElements(bot.getDriver(), this);
this.bot = bot;
}
 
public void clickLogin() {
bot.waitAndClick(loginButton, 5);
}
}

Instead of including the Bot code in every constructor, this code could be moved to another class that each component extends. This allows the individual component code to focus on details of the component and not on initialization code or passing the Bot around:

public class Component {
private Bot bot;
 
public Component(Bot bot) {
PageFactory.initElements(bot.getDriver(), this);
this.bot = bot;
}
 
public Bot getBot() {
return bot;
}
}
 
public class LoginComponent extends Component {
@FindBy(id = "login")
private WebElement loginButton;
 
public LoginComponent(Bot bot) {
super(bot);
}
 
public void clickLogin() {
getBot().waitAndClick(loginButton, 5);
}
}

Similarly, it is common for a component to verify that it is being instantiated at the right moment, to make it easier to debug when components are used incorrectly. We might want to check that the title is correct:

public class LoginPage extends Component {
public LoginPage(Bot bot) {
super(bot);
bot.waitForTitleContains("Please login");
}
}

Rather than include this call to the Bot in every class, we can move this check up into a specialized version of Component that other pages extend. This provides a small benefit that adds up when creating many Page Objects:

public class TitlePage extends Component {
public LoginPage(Bot bot, String title) {
super(bot);
bot.waitForTitleContains(title);
}
}
 
public class LoginPage extends TitlePage {
public LoginPage(Bot bot) {
super(bot, "Please login");
}
}

Other libraries provide helper classes for exactly this purpose. The Selenium Java library includes the LoadableComponentobject which abstracts the functionality and checks around loading a page.

The WebDriverExtensionsgoes even further by abstracting much of the code around Page Objects into annotations, creating simpler, easier to read components.

This 2-part blog only touched on a few useful techniques and design patterns, some information I’ve learned over the years to create better Selenium tests. Books have been written on the subject. Writing good tests means writing good software, and writing good software is a complex task. With the proper tools and knowledge we can improve the process and create stable, readable, and maintainable tests.

Tony Yoshicedo is a Senior Software Developer at Parasoft
Share this

Industry News

August 29, 2024

Progress announced the latest release of Progress® Semaphore™, its metadata management and semantic AI platform.

August 29, 2024

Elastic, the Search AI Company, announced the Elasticsearch Open Inference API now integrates with Anthropic, providing developers with seamless access to Anthropic’s Claude, including Claude 3.5 Sonnet, Claude 3 Haiku and Claude 3 Opus, directly from their Anthropic account.

August 28, 2024

Broadcom unveiled VMware Cloud Foundation (VCF) 9, the future of VCF that will accelerate customers’ transition from siloed IT architectures to a unified and integrated private cloud platform that lowers cost and risk.

August 27, 2024

Broadcom announced VMware Tanzu Platform 10, a cloud native application platform that accelerates software delivery, providing platform engineering teams enhanced governance and operational efficiency while reducing toil and complexity for development teams.

August 26, 2024

Red Hat announced the general availability of Red Hat OpenStack Services on OpenShift, the next major release of Red Hat OpenStack Platform.

August 26, 2024

Salesforce announced new innovations in Slack that make it easier for users to build automations, no matter their technical expertise.

August 26, 2024

GitLab announced the general availability of the GitLab Duo Enterprise add-on.

August 26, 2024

Tigera now delivers universal microsegmentation capabilities with Calico.

August 22, 2024

Tabnine announced a new platform partnership with Broadcom Inc., an integration with IBM, as well as continuing extensions of existing partnerships with Amazon Web Services (AWS), DigitalOcean, Google Cloud, and Oracle Cloud Infrastructure (OCI).

August 22, 2024

Wallarm released API Attack Surface Management (AASM), an agentless technology to help organizations identify, analyze, and secure their entire API attack surface.

August 21, 2024

LambdaTest launched KaneAI, an end-to-end software AI Test Agent.

August 20, 2024

Kubiya has closed its $12 million seed round with a $6 million extension of equity and debt financing and launched a paradigm-breaking new platform, AI Teammates, that enables true delegation of complex tasks to digital colleagues through organic, human-like conversations.

August 19, 2024

The Cloud Native Computing Foundation® (CNCF®), which builds sustainable ecosystems for cloud native software, announced the schedule for KubeCon + CloudNativeCon North America 2024, happening in Salt Lake City, Utah from November 12 – 15.

August 19, 2024

Diagrid announced the latest version of Dapr, a Cloud Native Computing Foundation incubating project maintained by Diagrid, Microsoft, Intel, Alibaba, and others, as well as an update to Conductor, a Software as a Service (SaaS) that helps manage, upgrade, and monitor Dapr on Kubernetes clusters.

August 15, 2024

Spectro Cloud announced two new formal recognitions of its strengthening position in the government technology space: the Government Software competency from AWS, and ‘Awardable’ status on the CDAO Tradewinds Solutions Marketplace for AI/ML solutions at the tactical edge.