Design Patterns in Selenium
By Pooja Deshpande, Community Contributor - February 20, 2023
Design patterns are reusable solutions that can be reused on to problems that frequently arise in software design. They serve as a common vocabulary and a shared understanding of best practices among the developers. Design Patterns help in improving the overall quality and maintainability of a codebase. They are not specific to any programming language and can be implemented in any language, although some patterns are more commonly used in certain languages.
There are several types of design patterns, such as
- Creational patterns
- Structural patterns
- Behavioral patterns
Selenium is an open-source tool for automating web browsers, and it can be used to create automated tests for web applications. There are several design patterns in Selenium that can when designing and implementing tests with Selenium,:
- Singleton Design Pattern: The Singleton design pattern ensures that no more than one instance of a class is created. It is often used when a single instance of an object is required to coordinate actions across the system. For example, in Selenium, the WebDriver object is typically a Singleton, as there should be only one instance of the browser for the entire test run.
- Page Object Model: The Page Object Model is used in automation testing where each web page in a web application is represented as a class. The class contains the elements and actions that can be performed on the page. This makes the test code more maintainable, as changes to the page can be made in one place instead of in multiple tests.
- Fluent Page Object Model: The Fluent Page Object Model is an extension of the Page Object Model, where methods are chained together to form a fluent interface. This makes the test code more readable and concise, as multiple actions can be performed on the page in a single line of code.
- Factory Design Pattern: This is used to create instances of classes. It is often used when a single class is not enough to create the required objects, and multiple subclasses are required. This pattern provides a way to encapsulate the object creation process and makes it easier to change the object creation process without affecting the rest of the code.
- Facade Design Pattern: The Facade design pattern provides a simplified interface to a complex system. It is used to make it easier to use the system by hiding its complexity behind a single interface. The Facade pattern can be used in Selenium to provide a simplified API for interacting with the browser, making it easier to write tests that are maintainable and easy to read.
With the help of these design patterns, developers can leverage solved solutions to common problems rather than reinventing the wheel each time they encounter a new challenge. This can lead to faster and more efficient development, as well as a more consistent and maintainable codebase.
Let’s see different Design patterns and how to implement them.
1. Singleton Design Pattern
Singleton class is a special class that can have only one object/instance of itself. When you design a class in such a way that it has only one object, it is called a singleton design pattern. The use of a singleton class is that you need to track only a single object across all classes in a framework.
There are certain points that one needs to consider while creating a singleton class –
- Constructor of the singleton class should be declared as private so that it can’t be instantiated outside the class.
- Write a static method that has a return type of object of this singleton class. This is also called Lazy Initialization.
Let’s see an example of Singleton class –
public class Singleton { private static Singleton singleton_ref = null; //declared constructor as private private Singleton() { System.out.Println("This is Singleton Class"); } //declared static method that returns the object of singleton class public static Singleton getInstance() { if(singleton_ref == null) singleton_ref = new Singleton(); return singleton_ref; } public static void main(String[] args) { Singleton a = Singleton.getInstance(); Singleton b = Singleton.getInstance(); } }
Output- As you can see, the output of the above program has printed the string only once even though there are two instances of the class. This means the constructor was called only once. This is because of the singleton pattern, as it will not create an object of class again if it is already initialized.
At the framework level, one can use a singleton class by creating a Webdriver instance only once in a base class and using the same instance across all the child classes that inherit the base class.
2. Page Object Model
Page Object Model in Selenium (POM) is the most popular design pattern used in web automation frameworks. POM makes our code easy to use and maintain by separating the test classes and page classes from each other. For this pattern, you need to create a separate class for each page in a web application. In these page classes, you can define page objects and the corresponding methods that implement those page objects. This way, it becomes easier to maintain our code and make any changes if required at any point in time without hampering the existing code.
This is how the framework structure for POM looks like –
In src/main/java – has page classes, and in src/test/java – it has test classes.Let us see the code snippets for the framework shown above
LoginPage :
import java.time.Duration; import java.util.Properties; import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import com.qa.hubspot.base.BasePage; import com.qa.hubspot.util.Constants; import com.qa.hubspot.util.ElementActions; public class LoginPage extends BasePage { WebDriver driver; ElementActions elementActions; //1. Create OR/Page objects -> using by locator By userinputbox = By.xpath("//input[@id='react-select-2-input']"); By paaswordinputbox = By.xpath("//input[@id='react-select-3-input']"); By loginBtn = By.xpath("//button[@id='login-btn']"); //2. Define a constructor public LoginPage(WebDriver driver) { this.driver = driver; elementActions = new ElementActions(driver); } //3. Page Actions/Methods public String getLoginPageTitle() { return elementActions.waitForPageTitle(Constants.LOGIN_PAGE_TITLE); } public HomePage doLogin(String username,String pwd) { elementActions.doSendkeys(userinputbox, username); driver.findElement(userinputbox).sendKeys(Keys.TAB); elementActions.doSendkeys(paaswordinputbox, pwd); driver.findElement(paaswordinputbox).sendKeys(Keys.TAB); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30)); elementActions.doClick(loginBtn); return new HomePage(driver); } } HomePage - import java.util.Properties; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import com.qa.hubspot.base.BasePage; import com.qa.hubspot.util.Constants; import com.qa.hubspot.util.ElementActions; public class HomePage extends BasePage{ WebDriver driver; Properties prop; ElementActions elementActions; //locators By BSLogo = By.xpath("//a[@class='Navbar_logo__26S5Y']"); By accountName = By.xpath("//span[contains(text(),'demouser')]"); public HomePage(WebDriver driver) { this.driver = driver; elementActions = new ElementActions(driver); } //Page Actions public String getHomePageTitle() { return elementActions.waitForPageTitle(Constants.HOME_PAGE_TITLE); } public boolean isHomePageLogoVisible() { return elementActions.isElementDisplayed(BSLogo); } public boolean isAccountNameVisible() { return elementActions.isElementDisplayed(accountName); } public String getAccountNameText() { return elementActions.doGetText(accountName); } }
Read More: Page Object Model in Selenium Python
3. Fluent Page Object Model
Fluent Page Object Model is a further extension to Page Object Model, which is more efficient and readable by implementing page objects with Fluent API.
Read More: Page Object Model in Selenium and JavaScript
Fluent Interface is implemented using method chaining and is the implementation of an API that provides the most readable code. The main objective of the Fluent Page Object Model pattern is to provide method chaining and make our code more readable and easy to use.
In the Fluent POM design pattern, method chaining is achieved by making methods of a page return the current instance of that page. However, it’s not mandatory that each method should return the current page object.
Let’s see an example of a CRMPRO web application that implements fluent POM by creating Login and homepage.
AUT – https://bstackdemo.com/signin
Code Snippet for BrowserStack demo Login Page –
import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; public class BSLoginPage { WebDriver driver; By usernm = By.xpath("//input[@id='react-select-2-input']"); By password = By.xpath("//input[@id='react-select-3-input']"); By loginBtn = By.xpath("//button[@id='login-btn']"); public BSLoginPage(WebDriver driver) { this.driver = driver; } public BSLoginPage enterUsername(String username) { driver.findElement(usernm).sendKeys(username); driver.findElement(usernm).sendKeys(Keys.TAB); return this; } public BSLoginPage enterPassword(String passwd) { driver.findElement(password).sendKeys(passwd); driver.findElement(password).sendKeys(Keys.TAB); return this; } public HomePage clickLoginBtn() { driver.findElement(loginBtn).click(); return new HomePage(driver); } }
As you can see in the above class, methods enterUsername(), enterPassword() return “this”. However, the method clickLoginBtn() returns the object of the HomePage class. This means it is not mandatory that every method should return “this” in fluent POM.
Code snippet for HomePage class –
import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.interactions.Actions; public class HomePage { By offers = By.xpath("//a[@id='offers']"); WebDriver driver; public HomePage(WebDriver driver) { this.driver = driver; } private HomePage clickOnOffers() { driver.findElement(offers).click(); return this; } }
Now Let’s create a test class to see the method chaining concept –
import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import FreeCRM.BSLoginPage; import io.github.bonigarcia.wdm.WebDriverManager; public class FluentDesignTest { WebDriver driver; BSLoginPage login; @BeforeTest public void setup() { WebDriverManager.chromedriver().setup(); WebDriver driver = new ChromeDriver(); driver.get("https://bstackdemo.com/signin"); login = new BSLoginPage(driver); } @Test public void LoginTest() { login.enterUsername("demouser") .enterPassword("testingisfun99") .clickLoginBtn(); } }
As you can see in the above test class, you don’t have to call each method separately by creating objects, instead in a single chain all the methods of a class are being called.
4. Factory Design Pattern
In the Factory Design Pattern, there is a class with a factory method that is responsible for performing all the complex actions required. For Example, if there are an ‘n’ number of classes then the factory class is responsible to instantiate all these classes. So we don’t have to deal with these many classes as the factory class will be responsible to handle them. You just have to create an object of the Factory class, and it will in turn instantiate the class as per the user input given at the test class level.
The best example of factory design pattern would be a factory class which is used to initialize a driver based on the user requirement. As seen in the POM design pattern example, Base class can act as Factory class in Factory design pattern.
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Properties; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.firefox.FirefoxDriver; import io.github.bonigarcia.wdm.WebDriverManager; public class BasePage { WebDriver driver; Properties prop; public WebDriver init_driver(Properties prop) { String browser = prop.getProperty("browser"); if(browser.equals("chrome")) { WebDriverManager.chromedriver().setup(); driver = new ChromeDriver(); } else if(browser.equals("firefox")) { WebDriverManager.firefoxdriver().setup(); driver = new FirefoxDriver(); } else { System.out.println("Please provide a proper browser value.."); } driver.manage().window().fullscreen(); driver.get(prop.getProperty("url")); return driver; } public Properties init_properties() { prop = new Properties(); try { FileInputStream ip=new FileInputStream("src/main/java/com/qa/hubspot/config/config.properties"); prop.load(ip); } catch (FileNotFoundException e) { e.printStackTrace(); }catch (IOException e) { e.printStackTrace(); } return prop; } }
5. Facade Design Pattern
Facade Design Pattern is a structural design pattern. Facade as the name says, represents the face of a building. A facade hides the complexity of a system and provides a simple interface to the end user. For example, a Computer comes with a CPU, hard disk, memory etc. When you start the computer you have no idea what happens internally for the startup. This is how a facade works. You can implement facade by creating a facade class and write all the business logic inside a method. The facade acts as an entry point to the subsystems. You can go for the Facade design pattern when we have a complex application and want to provide a simplified interface to the end user.
Let’s see an example of a facade –
1. Create an Interface
public interface Car{ void drive(); }
2. Create classes that implement the interface
public class BMW implements Car{ public void drive() { System.out.println("BMW::drive()"); } } public class Audi implements Car{ public void drive() { System.out.println("Audi::drive()"); } } public class Creta implements Car{ public void drive() { System.out.println("Creta::drive()"); } }
3. Create a facade class
public class DriveCar{ private Car bmw; private Car audi; private Car creta; public DriveCar() { bmw = new BMW(); audi = new Audi(); creta = new Creta(); } public void driveBMW(){ bmw.drive(); } public void driveAudi(){ audi.drive(); } public void driveCreta(){ creta.drive(); } }
4. Use the facade class to call the methods
public class FacadeDemo { public static void main(String[] args) { DriveCar c = new DriveCar(); c.driveBMW(); c.driveAudi(); c.driveCreta(); } }
You can choose any design pattern from the above mentioned depending upon the complexity and ease of use of the system. All design patterns are very helpful in making our automation framework less complex and easy to use.
BrowserStack gives you instant access to Selenium Grid of 3000+ real devices and desktop browsers. Running your Selenium tests on BrowserStack is simple yet effective, always.