Selenium Parallel testing using Java ThreadLocal and TestNG
Tags: testing, selenium, java, testng
Published: 2022-06-10 13:22:41

No, writing just `parallel = “tests”` & `thread-count =”2"` in your testng.xml file won’t cut it. That is because Selenium WebDriver is not thread-safe by default. In a non-multithreaded environment, we keep our WebDriver reference static to make it thread-safe. But the problem occurs when we try to achieve parallel execution of our tests within the framework. Every thread you create to parallelize your tests tries to overwrite the WebDriver reference since there could be only one instance of static WebDriver reference.

To overcome this problem, we can take help from the ThreadLocal class of Java. Wondering what is ThreadLocal? Simply put, it enables you to create a generic/ThreadLocal type of object which can only be read and written (via its get and set method) by the same thread, so if two threads are trying to read and write a ThreadLocal object concurrently, one thread would not see the modification of the ThreadLocal object done by the other thread, thus, making the thread-local object thread-safe.

A typical thread-local object is made private static and its class provides initialValue, get, set, and remove methods. The following example shows how you can create a Generic type thread-local object

```
//create a generic thread-local object
private static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
//set thread-local value
myThreadLocal.set("Hello ThreadLocal");
//get thread-local value
String threadLocalValue = myThreadLocal.get();
//remove thread-local value for the current thread
myTheadlocal.remove();
```

Now that we have seen how ThreadLocal works, we can go ahead and implement it in our Selenium framework making the WebDriver session exclusive to each thread. Let’s create a BroweserManager class to manage the WebDriver instance alongside browsers.

```
public class BrowserManager {
 
   public static WebDriver doBrowserSetup(String browserName){
        WebDriver driver = null;
        if (browserName.equalsIgnoreCase("chrome")){
            //steup chrome browser
            WebDriverManager.chromedriver().setup();
            //Add options for --headed or --headless browserlaunch
            ChromeOptions chromeOptions = new ChromeOptions();
            chromeOptions.addArguments("-headed");
            
            //initialize driver for chrome
            driver = new ChromeDriver(chromeOptions);
            //maximize window
            driver.manage().window().maximize();
            //add implicit timeout
            driver.manage()
           .timeouts()
           .implicitlyWait(Duration.ofSeconds(30));
        }
   return driver;
}
```

After that, it’s time to create our BaseTest class where we will create a static WebDriver as a ThreadLocal and then set the driver to its object which will be accessible throughout the test with the help of our BrowserManager class.

```
public class BaseTest {
    protected static 
    ThreadLocal<WebDriver> threadLocalDriver = new ThreadLocal<>();
    @BeforeTest
    public void Setup(){
        WebDriver driver = BrowserManager.doBrowserSetup("chrome");
        //set driver
        threadLocalDriver.set(driver);
        System.out.println("Before Test Thread ID: "+Thread.currentThread().getId());
        //get URL
        getDriver().get("https://www.linkedin.com/");
    }
    //get thread-safe driver
    public static WebDriver getDriver(){
        return threadLocalDriver.get();
    }
    @AfterTest
    public void tearDown(){
        getDriver().quit();
        System.out.println("After Test Thread ID: "+Thread.currentThread().getId());
        threadLocalDriver.remove();
    }
}
```

Inside @BeforeTest annotation we have called the doBrowserSetup() static method from BrowserManager class which returned a WebDriver reference with ChromeDriver initialized in it. Then we have set the returned WebDriver reference in our threadLocalDriver object. Now that our threadLocalDriver is set making the WebDriver reference thread-safe, we can use it across the test framework.

Inside @AfterTest annotation we are just quitting the browser and then removing the current thread’s value (in this case, returned WebDriver reference from the doBrowserSetup() static method) from the threadLocalDriver object.

Let’s complete the test by adding Test classes to it. We will run a login test on a demo site.

```
package Pages;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class Login {
    WebDriver driver;
    @FindBy(className = "login")
    WebElement linkLogin;
    @FindBy(id = "email")
    WebElement txtEmail;
    @FindBy(id = "passwd")
    WebElement txtPassword;
    @FindBy(id = "SubmitLogin")
    WebElement btnSignIn;
    @FindBy(xpath = "//span[contains(text(),'viva test')]")
    WebElement lblUserName;
    @FindBy(xpath = "//li[contains(text(),'Invalid email address.')]")
    WebElement lblInvalidEmail;
    @FindBy(xpath = "//li[contains(text(),'Authentication failed.')]")
    WebElement lblInvalidPassword;
    public Login(WebDriver driver){
        this.driver = driver;
        PageFactory.initElements(driver, this);
    }
    //valid email and valid password
    public String doLogin(String email, String password){
        linkLogin.click();
        txtEmail.sendKeys(email);
        txtPassword.sendKeys(password);
        btnSignIn.click();
        return lblUserName.getText();
    }
    //Invalid email
    public String loginWithInvalidPassword(String email, String password){
        linkLogin.click();
        txtEmail.sendKeys(email);
        txtPassword.sendKeys(password);
        btnSignIn.click();
        return lblInvalidPassword.getText();
    }
   
}
```

Now create a test class based on the above class i.e. LoginTestRunner1.

```
package TestRunners;
import Base.BaseTest;
import Pages.Login;
import Utils.BrowserManager;
import Utils.JsonReader;
import org.json.simple.parser.ParseException;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.io.IOException;
public class LoginTestRunner1 extends BaseTest {
    @Test
    public void loginTest() throws IOException, ParseException {
        getDriver().get("http://automationpractice.com/");
        Login login = new Login(getDriver());
        //valid email and valid password
        String user = login.doLogin("test_viva@test.com", "123456");
        Assert.assertEquals(user, "viva test");
    }
}
```


Create LoginTestRunner2 class:

```
package TestRunners;
import Base.BaseTest;
import Pages.Login;
import Utils.BrowserManager;
import Utils.JsonReader;
import org.json.simple.parser.ParseException;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.io.IOException;
public class LoginTestRunner2 extends BaseTest {
    @Test
    public void loginWithInvalidEmailTest() throws IOException, ParseException {
        
        getDriver().get("http://automationpractice.com/");
        Login login = new Login(getDriver());
        //invalid email and valid pass
        String lblInvalidEmail = login.loginWithInvalidEmail("viva@te.com", "123456");
        Assert.assertEquals(lblInvalidEmail, "Authentication failed.");
    }
}
```


Finally, let’s write parallel = “tests” & thread-count =”2" in our testng.xml file.

```
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd"> 
<suite  verbose="1" name="E-commerce portal automation" parallel="tests" thread-count="2" >
    <test name="invalid email login" >
        <classes>
            <class name="TestRunners.LoginTestRunner2"/>
        </classes>
    </test>
    <test name="valid email login">
        <classes>
            <class name="TestRunners.LoginTestRunner"/>
        </classes>
    </test>
</suite>
```

Now run the test. See the output below:

```
Before Test Thread ID: 15
Before Test Thread ID: 14
After Test Thread ID: 15
After Test Thread ID: 14
BUILD SUCCESSFUL in 48s
4 actionable tasks: 4 executed
```

That’s it. WebDriver instance has now become thread-safe, and as the output shows it is being used by the multiple threads concurrently thanks to ThreadLocal class.