2 Important Selenium design patterns and best practices

In this tutorial we will learn about Selenium design patterns and best practices while working with Selenium Automation framework development (hybrid framework in selenium), there two variations of Framework Design or Framework model that we have to consider, which are : 

We need to know and understand why Language design pattern is required when we are developing our framework in one of the Selenium framework Model. We would suggest to go through the previous segments of the Selenium Framework development tutorial series to get the whole understanding .

Let’s understand that in detail: 

selenium design patterns and best practiceshybrid framework in selenium

While designing any Framework, we need to consider some design architecture, i.e., selenium design patterns and best practices, and as per the need of the type of the framework model, we need to select a Language Design pattern to solve the problem state of the framework design as a whole.

Hence just to conclude, we can choose and Selenium framework model (Hybrid, Page Object Model, Data Driven, etc.), but to implement the model, we need to follow and implement some Language Design Pattern(e.g., java/C# Design patterns ) 

Why We need selenium design Pattern and best practices while building Selenium Framework: 

What design patterns to be used in Selenium Framework : 

There are a few design pattern you could use to implement different areas of the framework, such as an example : 

We will do the live coding template of the whole Framework in the upcoming posts here.

Singleton Design Pattern for hybrid framework in Selenium: 

Singleton Design Pattern is a pattern in which you could create only one object from a class and get to use the same object to access the methods of the class; we could use the design pattern in the configurator where we only need to read the configuration data and can load into some data store(any kind of data structure which you could use as and when required while in the execution from any classes and methods ) 

So we could achieve the same in the below manner while designing the same with Singleton Design pattern. 

NOTE: We will design and develop the framework from scratch in the upcoming section of the tutorial series, but this specific tutorial will provide you insight into the necessity of the design pattern.

package com.cyborg.core.generic.dataUtils;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Properties;
import java.util.Set;
import org.apache.log4j.PropertyConfigurator;
// This is the SingleTon Class 
public class PropertiesDataUtils {
   private Properties properties = null;
   public static LinkedHashMap<String, String> configDataStore = new LinkedHashMap<String, String>();
   InputStream is = null;
// This is the static and private reference of the class which you could use anywhere in you framework 
   private static PropertiesDataUtils propertiesDataUtils = null;
   boolean centralizeLog  = false;
// This is the Private constructor to create the object but you can not access this from outside the class to maintain the design of the SingleTon pattern ie only one object creation .
   private PropertiesDataUtils(String filePath) {
       generateDataStore(filePath);
       centralizeLog =  Boolean.parseBoolean(PropertiesDataUtils.configDataStore.get("centralizedLog"));
       if(centralizeLog)
           PropertyConfigurator.configure(System.getProperty("user.dir")+"//src//test//resources//config//log4j_central.properties");
         else
            PropertyConfigurator.configure(System.getProperty("user.dir")+"//src//test//resources//config//log4j_local.properties");
   }
   private PropertiesDataUtils() {
   }
  
 
// This method basically create the instance of the SingleTon class 
   public static PropertiesDataUtils getInstance(String filePath) {
       if (propertiesDataUtils == null)
           propertiesDataUtils = new PropertiesDataUtils(filePath);
       return propertiesDataUtils;
   }
// this method basically creates the datastore where you want to store all the config data as discussed previously 
   private void generateDataStore(String filePath) {
       try {
           this.properties = new Properties();
           is=new FileInputStream(filePath);
           properties.load(is);
           overrideFromEnvironment();
           Set<Object> keys = loadAllKeys();
           for (Object k : keys) {
               String key = (String) k;
               configDataStore.put(key, getPropertyValue(key));
           }
       } catch (FileNotFoundException fileNotFoundException) {
           String exceptionData = String.valueOf(fileNotFoundException.getCause().getMessage());
       } catch (IOException ioException) {
           String exceptionData = String.valueOf(ioException.getCause().getMessage());
       } finally {
           if (null != is) {
               try {
                   is.close();
               } catch (Exception e) {
                   String exceptionData = String.valueOf(e.getCause().getMessage());
               }
           }
       }
   }
// This method is used to load all the keys from the properties file.
   private Set<Object> loadAllKeys() {
       Set<Object> keys = this.properties.keySet();
       return keys;
   }
   private String getPropertyValue(String key) {
       return this.properties.getProperty(key);
   }
   private void setPropertyValue(String key,String value) {
        this.properties.setProperty(key,value);
   }
  
  private void overrideFromEnvironment() {
     if (this.properties.getProperty("run_on_jenkins").equals("true")) {
        System.out.println("SET run_on_jenkins FLAG TO FALSE IN YOUR PROPERTIES FILE TO RUN LOCALLY....");
        String build_number = System.getenv("BUILD_NUMBER");
        this.properties.setProperty("build_number", build_number);
     }
    
  }
}

By this approach, we could use the Singleton design pattern and use it in our framework.

Factory Design Pattern in Selenium Framework : 

In the factory design pattern, we create a class (we call it a Factory class ), and on the other hand, we have one interface and eventually implemented by “n” number of classes.

The factory class basically returns the object of the above classes (depending on the need), so you don’t have to deal with the above “n” number of classes’ object; rather, you can create one object of the Factory class and call the method of factory class which returns the needed baseline Object for the required classes among the adobe “n” classes.

Now, this design you can consider while creating the different Webdriver / browser implementation. 

We have a various browser and the implementation with a different type of Selenium driver (e.g., LocalDriver, RemoteDriver, ThreadDriver, etc.) and as and when you require a specific type of driver and specific type of browser you can mention in the configuration file and based on the need the factory class will provide you the instance of the driver and browser for your automation script to use further. 

Here is the code base for implementing this design pattern while creating driver -browser interactions : 

Interface Design : 

package com.cyborg.core.web.utils.driverUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
public interface IDriver {
   public WebDriver init(String browserName);
}

“N” number of browse classes implementation (who are implementing the interface ) :

package com.cyborg.core.web.utils.driverUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.safari.SafariDriver;
public class LocalDriver implements IDriver {
   public WebDriver init(String browserName) {
       String pathToDriver = getDriverPath(browserName);
       if (null != browserName) {
           switch (browserName) {
               case "chrome":
                   System.setProperty("webdriver.chrome.driver",
                           pathToDriver);
                   return new ChromeDriver();
               case "firefox":
                   System.setProperty("webdriver.gecko.driver", pathToDriver);
                   return new FirefoxDriver();
               default:
                   System.setProperty("webdriver.chrome.driver", pathToDriver);
                   return new ChromeDriver();
           }
       } else {
           System.setProperty("webdriver.chrome.driver",
                   pathToDriver);
           return new ChromeDriver();
       }
   }
   private String getDriverPath(String browserName) {
       String osData = System.getProperty("os.name").toLowerCase().split("\\\\s")[0];
       if (null != osData) {
           if (osData.equalsIgnoreCase("mac")) {
               return "./DriversExe/" + osData + "_" + browserName;
           } else if (osData.contains("nux") || (osData.contains("nix"))) {
               return "./DriversExe/linux_" + browserName;
           } else if (osData.contains("win")) {
               return "./DriversExe/" + osData + "_" + browserName + ".exe";
           }
       }
       return null;
   }
}

Here is the implementation of the Remote Driver class : 

package com.cyborg.core.web.utils.driverUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import com.cyborg.core.generic.dataUtils.PropertiesDataUtils;
import java.net.MalformedURLException;
import java.net.URL;
public class RemoteDriver implements IDriver {
   DesiredCapabilities caps;
   String remoteHuburl=PropertiesDataUtils.configDataStore.get("WEB_GRID_IP");
   @Override
   public WebDriver init(String browserName) {
       if (browserName != null) {
           switch (browserName) {
               case "firefox":
                   try {
                       return new RemoteWebDriver(new URL(remoteHuburl), caps.firefox());
                   } catch (MalformedURLException malformedUrlEx) {
                       malformedUrlEx.getCause().getMessage();
                       malformedUrlEx.printStackTrace();
                   }
               case "chrome":
                   try {
                       return new RemoteWebDriver(new URL(remoteHuburl), caps.chrome());
                   } catch (MalformedURLException malformedUrlEx) {
                       malformedUrlEx.getCause().getMessage();
                       malformedUrlEx.printStackTrace();
                   }
               case "ie":
                   try {
                       return new RemoteWebDriver(new URL(remoteHuburl), caps.internetExplorer());
                   } catch (MalformedURLException malformedUrlEx) {
                       malformedUrlEx.getCause().getMessage();
                       malformedUrlEx.printStackTrace();
                   }
               default:
                   try {
                       return new RemoteWebDriver(new URL(remoteHuburl), caps.chrome());
                   } catch (MalformedURLException malformedUrlEx) {
                       malformedUrlEx.getCause().getMessage();
                       malformedUrlEx.printStackTrace();
                   }
           }
           return null;
       } else {
           return null;
       }
   }

Here is the implementation of the Factory class, which provides the respective browser and driver class object : 

package com.cyborg.core.web.utils.driverUtils;
public class DriverProvider {
   public IDriver getDriver(String typeOfDriver) {
       if (typeOfDriver != null) {
           switch (typeOfDriver) {
               case "local":
                   return new LocalDriver();
               case "remote":
                   return new RemoteDriver();
              
               default:
                   return new LocalDriver();
           }
       } else {
           return null;
       }
   }
}

Likewise, you can implement the Appium driver along with the same design, just provide the implementation and declare a method in the IDriver interfaces. 

Conclusion: With this, we are concluding here how you can use language design patterns as part of the Selenium design patterns and best practices while developing the hybrid framework in Selenium; in the upcoming segments of the tutorial, we will build the Page Object model framework for Selenium Automation.

To get the Overall tutorial on Selenium, you can visit here

Leave a Comment