Search code examples
javaselenium-webdriverparallel-processingcucumber-jvmthread-local

Parallel run of Selenium tests (uses ThreadLocal) results in orphaned browsers being opened


I use ThreadLocal for thread safety and run the tests in parallel using Maven failsafe and JUnit. I am running two tests from two feature files to test parallel running.

But I always have the first browser blank. Then the subsequent ones are fine and the tests pass. If I run sequentially, there isn’t any issue.

HookStep class:

    public class HookStep {

        @Before()
        public void beginTest() {
            WebDriverFactory.setDriver(Props.getValue("browser.name"));
        }

        @After()
        public void stopTest(Scenario scenario) {
            switch (environment) {

                case "local":
                case "aws": {
                    if (scenario.isFailed()) {
                        Screenshots.Shot shot = new Screenshots(Screenshots.CONTEXT_TEST_FAIL)
                                .takeShot(scenario.getName() + formCounter.getAndIncrement() + "");
                        scenario.embed(shot.getContent(), "image/png", "Error - ");
                    }
                    WebDriverFactory.closeBrowser();
                }
            }
}

WebDriverFactory class:

public class WebDriverFactory {

    private static ThreadLocal<WebDriver> driver = new ThreadLocal<>();

    public static synchronized void setDriver(String browser) {

        switch (browser) {
            case "chrome":
                driver = ThreadLocal.withInitial(() -> {
                    WebDriverManager.chromedriver().setup();
                    return new ChromeDriver(BrowserOptions.getChromeOptions());
                });
                prepareBrowser();
                break;

            case "fireFox":
                driver = ThreadLocal.withInitial(() -> {
                    WebDriverManager.firefoxdriver().setup();
                    return new FirefoxDriver(BrowserOptions.getFirefoxOptions());
                });
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + browser);
        }
    }

    private static void prepareBrowser() {
        getDriver().manage().window().maximize();
        getDriver().manage().deleteAllCookies();
        getDriver().manage().timeouts().pageLoadTimeout(15, TimeUnit.SECONDS);
        getDriver().manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);
    }

    public static synchronized WebDriver getDriver() {
        return driver.get();
    }

    public static void closeBrowser() {

        getDriver().quit();
    }
}

The StepDef class:

public class SampleStepDef {
    private final WorldHelper helper;

    public SampleStepDef(WorldHelper helper) {
        this.helper = helper;
    }

    @Given("I click on the URL")
    public void iClickOnTheURL() {

       helper.getSamplePage().navigateToSite();
    }
}


public class WorldHelper {
    WebDriverFactory webDriverFactory = new WebDriverFactory();

    protected  WebDriver webDriver = webDriverFactory.getDriver();
    private  BasePage basePage;
    private  SamplePage samplePage;

    public SamplePage getSamplePage() {
        if(samplePage != null)
            return samplePage;
        samplePage = PageFactory.initElements(webDriver, SamplePage.class);
        return samplePage;
    }

}


public class SamplePage extends BasePage {

    public SamplePage(WebDriver webDriver) {
        super(webDriver);
    }

    public void navigateToSite() {

        webDriver.get("https://www.bbc.co.uk");
        webDriver.findElement(By.xpath("//a[contains(text(),\'News\')]")).click();
    }
}


public class BasePage extends WorldHelper {

    public BasePage(WebDriver driver) {
        this.webDriver = driver;
    }
}

How can I fix this problem?


Solution

  • I noticed multiple problems associated with your code.

    1. You are making use of ThreadLocal.withInitial(). Ideally speaking this should have been defined when you are instantiating the driver thread local static variable.

      So instead of

      private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
      

      it should have been

      private static final ThreadLocal<WebDriver> driver = ThreadLocal.withInitial(() -> {
          return null; //Your supplier goes here.
      });
      
    2. There's a clear mess up in your inheritance hierarchy (there's a very good chance that you were trying to create a simple example and perhaps have omitted out the details behind the layers of inheritance), but it wasn't clear as to why does all your page object classes extend WorldHelper

    3. You are having multiple statements at the class level such as this. The problem with these field level initialisations is that they get invoked when the object is constructed. So if the object is being constructed in a different thread, then you run into the problem of the WebDriver initialisation being triggered for that thread. End result: You have a lot of ghost browser instances that keep getting opened up, but no selenium actions are directed to them.

      private final WebDriver driver = WebDriverFactory.getDriver();
      

      When working with ThreadLocal variants of WebDriver management, you need to make sure that your calls are always from within your step definitions and never from the constructor or from class level field initialisations such as above.

    Here are the list of fixes that you need to do.

    1. Remove all occurrences of private final WebDriver driver = WebDriverFactory.getDriver(); in your code. They are not needed.

    2. Refactor your WebDriverFactory class to look like below (For brevity I have removed off all the commented out code)

       public class WebDriverFactory {
      
           private static final ThreadLocal<WebDriver> driver = new ThreadLocal<>();
      
           public static void setDriver(String browser) {
               RemoteWebDriver rwd;
                   switch (browser) {
                   case "chrome":
                       WebDriverManager.chromedriver().setup();
                       rwd = new ChromeDriver(BrowserOptions.getChromeOptions());
                       break;
      
                   case "fireFox":
                       WebDriverManager.firefoxdriver().setup();
                       rwd = new FirefoxDriver(BrowserOptions.getFirefoxOptions());
                       break;
                   default:
                       throw new IllegalStateException("Unexpected value: " + browser);
               }
               driver.set(Objects.requireNonNull(rwd));
               prepareBrowser();
           }
      
           private static void prepareBrowser(){
               getDriver().manage().window().maximize();
               getDriver().manage().deleteAllCookies();
               getDriver().manage().timeouts().pageLoadTimeout(15, TimeUnit.SECONDS);
               getDriver().manage().timeouts().implicitlyWait(2, TimeUnit.SECONDS);
           }
      
           public static WebDriver getDriver(){
               return Objects.requireNonNull(driver.get());
           }
      
           public static void closeBrowser() {
               getDriver().manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
               getDriver().close();
               getDriver().quit();
           }
       }
      
    3. Since all your page classes seem to be extending from WorldHelper, add a getter method such as below in it (or) ensure that no where in any of your page classes you have a WebDriver field. Whenever you need to get hold of the WebDriver instance, you should do it directly via WebDriverFactory.getDriver() (or) via the getter method such as below in your WorldHelper or whatever base class you are creating.

       protected WebDriver getDriver() {
           return WebDriverFactory.getDriver();
       }
      

    Once you have fixed the afore-mentioned problems, you should be good and shouldn't see any blank browser windows open up.

    Note: Please clean up your project on GitHub. I noticed some cloud service provider credentials in it (it could be real credentials or could be fake. I wouldn't know.)