You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ThreadLocal与PageFactory实现Selenium并行执行的线程安全问题排查

问题:ThreadLocal实现Selenium并行执行时的线程安全问题

并行执行时出现以下异常现象:

  • 启动2个Chrome浏览器
  • 两个浏览器均点击了注册链接
  • 仅一个浏览器点击登录链接,且该浏览器重复点击登录链接出现报错
  • 另一个浏览器完全未点击登录链接

相关代码实现

DriverFactory类

public class DriverFactory {

//Singleton design Pattern
//private constructor so that no one else can create object of this class
private DriverFactory() {
    
}

private static DriverFactory instance  = new DriverFactory();

public static DriverFactory getInstance() {
    return instance;
}


//factory design pattern --> define separate factory methods for creating objects and create objects by calling that methods
ThreadLocal<WebDriver> driver = new ThreadLocal<WebDriver>();

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

public void
setDriver(WebDriver driverParm) {
    driver.set(driverParm);
    System.out.println("Before Test Thread ID: "+Thread.currentThread().getId());
}


public void closeBrowser() {
    driver.get().quit();
    System.out.println("After Test Thread ID: "+Thread.currentThread().getId());
    driver.remove();
}
}

BrowserFactory类

public class BrowserFactory {

//create webdriver object for given browser
public WebDriver createBrowserInstance(String browser) throws MalformedURLException {

    WebDriver driver = null;
    //RemoteWebDriver driver = null;


    if(browser.equalsIgnoreCase("Chrome")) {

        WebDriverManager.chromedriver().setup();
        System.setProperty("webdriver.chrome.silentOutput", "true");
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--incognito");
        DriverFactory.getInstance().setDriver(driver);
        driver = new ChromeDriver(options);

    }else if (browser.equalsIgnoreCase("firefox")) {

        WebDriverManager.firefoxdriver().setup();
        FirefoxOptions foptions = new FirefoxOptions();
        foptions.addArguments("-private");
        
        //driver = new RemoteWebDriver(new URL("http:192.168.225.219:4444/wd/hub"), DesiredCapabilities.firefox());

        
        driver = new FirefoxDriver(foptions);

    } if (browser.equalsIgnoreCase("ie")) {

        WebDriverManager.iedriver().setup();
        InternetExplorerOptions iOptions = new InternetExplorerOptions();
        iOptions.addCommandSwitches("-private");

        driver = new InternetExplorerDriver(iOptions);
    }
    return driver;
}
}

TestBase类

public class TestBase extends ActionEngine {
public WebDriver driver;
public BrowserFactory browserFactory;
String browserName = null;
//static ExtentReports extent = ExtentManager.getInstance();

/*public WebDriver getDriver() {
    driver=DriverFactory.getInstance().getDriver();
    return driver;
}*/


@BeforeMethod
public void LaunchApplication() throws Exception {
    browserName = PropertiesOperations.getPropertyValueByKey("browser");
    browserFactory = new BrowserFactory();
    DriverFactory.getInstance().setDriver(browserFactory.createBrowserInstance(browserName));
    //driver = browserFactory.initBrowser(browserName);
    //driver = DriverFactory.getInstance().getDriver();
    String url = PropertiesOperations.getPropertyValueByKey("url");
    DriverFactory.getInstance().getDriver().get(url);
    Thread.sleep(5000);
    DriverFactory.getInstance().getDriver().manage().window().maximize();
    System.out.println("Browser maximized");
    DriverFactory.getInstance().getDriver().manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);

}

@AfterMethod
public void tearDown() {
    DriverFactory.getInstance().closeBrowser();
}

//@AfterMethod
public void assignDevice() {
    ExtentFactory.getInstance().getExtent().assignDevice(browserName);
}

//@AfterMethod
public void assignAuthor() {
    ExtentFactory.getInstance().getExtent().assignAuthor("Mayank Mishra");

}

}

TestNG.xml

<suite name="Demo Web App test suite" parallel="methods" thread-count="2" >
<listeners>
    <listener
        class-name="reusableComponents.ListenersImplementation" />
    <listener
        class-name="reusableComponents.TestRetryAnalyzerListener" />
</listeners>
<test name="LoginTests">
    <classes>
        <class name="Tests.LoginTest" />
    </classes>
</test> <!-- Test -->
    <!--<test name="DataDriven Tests">
    <classes>
    <class name="Tests.TestCase" /> 
    </classes>
</test>--> <!-- Test -->
</suite> <!-- Suite -->

RegistrationPage类

public class RegistrationPage {
private WebDriver driver;

//Asserssion asserssion;

public RegistrationPage(WebDriver driver) {
    //driver=DriverFactory.getInstance().getDriver();
    this.driver = driver;
    PageFactory.initElements(driver, this);
}

@FindBy(linkText = "ACCOUNT")
private WebElement accountLink;
@FindBy(linkText = "Register")
private WebElement registerLink;
@FindBy(linkText = "Log In")
private WebElement loginLink;
@FindBy(xpath = "//h3[contains(text(),'Contact Information')]/following-sibling::a")
private WebElement editAccountInfo;
@FindBy(xpath = "//a[contains(text(),'Forgot Your Password?')]")
private WebElement forgotPasswordLink;
//public static Logger logger = Logger.getLogger(RegistrationPage.class.getName());

//public static Logger log = Logger.getLogger("");
public  void clickAccount() throws InterruptedException {
    Thread.sleep(5000);
    accountLink.click();
    //loginLink.click();
    //forgotPasswordLink.click();
    //loginLink.click();
    //driver=DriverFactory.getInstance().getDriver();
    //driver.findElement(By.linkText("ACCOUNT")).click();
    ExtentFactory.getInstance().getExtent().log(INFO,"Click on account link");
    //Log.info("clicked on account link");
}


public  void clickLoginLink() throws InterruptedException {
    //Log.info("Click on login link");
    Thread.sleep(5000);
    loginLink.click();
    ExtentFactory.getInstance().getExtent().log(INFO,"Click on login link");
}

public void clickForgotPasswordLink() throws InterruptedException {
    //Log.info("Click on login link");
    Thread.sleep(5000);
    forgotPasswordLink.click();
    ExtentFactory.getInstance().getExtent().log(INFO,"Click on forgot password link");
}


}

LoginTest类

public class LoginTest extends TestBase {
LoginPage loginPage;
RegistrationPage registrationPage;
ExcelOperations excel = new ExcelOperations("validLogin");
ExcelOperations excel2 = new ExcelOperations("invalidLogin");


//Dataprovider method --> return object array
@DataProvider(name = "validLogin")
public Object[][] testDataSupplier1() throws Exception {
    Object[][] obj = new Object[excel.getRowCount()][1];
    for (int i = 1; i <= excel.getRowCount(); i++) {
        HashMap<String, String> testData = excel.getTestDataInMap(i);
        obj[i - 1][0] = testData;
    }
    return obj;

}

@DataProvider(name = "invalidLogin")
public Object[][] testDataSupplier2() throws Exception {
    Object[][] obj = new Object[excel2.getRowCount()][1];
    for (int i = 1; i <= excel2.getRowCount(); i++) {
        HashMap<String, String> testData = excel2.getTestDataInMap(i);
        obj[i - 1][0] = testData;
    }
    return obj;

}

@BeforeMethod
public void loadClass() {
    //loginPage = PageFactory.initElements(DriverFactory.getInstance().getDriver(), LoginPage.class);
    registrationPage = PageFactory.initElements(DriverFactory.getInstance().getDriver(), RegistrationPage.class);
}

@Test(dataProvider = "invalidLogin", description = "login with invalid password")
public void loginTest_01(Object obj1) {
    try {
        System.out.println("in method1");
        registrationPage.clickAccount();
        System.out.println("clicking on login link");
        registrationPage.clickLoginLink();
       
    } catch (Exception e) {
        System.out.println(e.getMessage());
        Assert.fail("Cant do login");
    }

}

@Test(dataProvider = "validLogin", description = "login with valid password")
public void loginTest_02(Object obj2) {
    try {
        System.out.println("in method2");
        registrationPage.clickAccount();
        System.out.println("clicking on login link");
        Thread.sleep(5000);
        registrationPage.clickLoginLink();
       
    } catch (Exception e) {
        System.out.println(e.getMessage());
        Assert.fail("Cant do login");
    }

}

}

问题排查与修复

1. BrowserFactory中的致命错误

在Chrome分支中,你先调用DriverFactory.getInstance().setDriver(driver);,但此时driver还是null,之后才创建new ChromeDriver(options)并赋值。这导致ThreadLocal存储的是null,后续操作会出现线程间driver引用混乱。

修复:
将setDriver移到driver实例化之后,同时给Firefox和IE分支补充setDriver调用:

if(browser.equalsIgnoreCase("Chrome")) {
    WebDriverManager.chromedriver().setup();
    System.setProperty("webdriver.chrome.silentOutput", "true");
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--incognito");
    driver = new ChromeDriver(options);
    DriverFactory.getInstance().setDriver(driver); // 移到实例化后
}
else if (browser.equalsIgnoreCase("firefox")) {
    WebDriverManager.firefoxdriver().setup();
    FirefoxOptions foptions = new FirefoxOptions();
    foptions.addArguments("-private");
    driver = new FirefoxDriver(foptions);
    DriverFactory.getInstance().setDriver(driver); // 新增
} 
if (browser.equalsIgnoreCase("ie")) {
    WebDriverManager.iedriver().setup();
    InternetExplorerOptions iOptions = new InternetExplorerOptions();
    iOptions.addCommandSwitches("-private");
    driver = new InternetExplorerDriver(iOptions);
    DriverFactory.getInstance().setDriver(driver); // 新增
}

2. ThreadLocal变量的可见性优化

DriverFactory中的ThreadLocal变量driver应声明为private static,确保单例实例中的ThreadLocal全局唯一且线程安全:

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

3. RegistrationPage构造方法优化

当前构造方法依赖外部传入driver,改为直接从ThreadLocal获取,避免线程间传递错误实例:

public RegistrationPage() {
    this.driver = DriverFactory.getInstance().getDriver();
    PageFactory.initElements(driver, this);
}

对应LoginTest的loadClass方法修改为:

@BeforeMethod
public void loadClass() {
    registrationPage = new RegistrationPage();
}

4. TestBase冗余代码清理

移除TestBase中未使用的public WebDriver driver;成员变量,简化LaunchApplication方法:

@BeforeMethod
public void LaunchApplication() throws Exception {
    browserName = PropertiesOperations.getPropertyValueByKey("browser");
    browserFactory = new BrowserFactory();
    browserFactory.createBrowserInstance(browserName); // 内部已完成setDriver
    WebDriver driver = DriverFactory.getInstance().getDriver();
    String url = PropertiesOperations.getPropertyValueByKey("url");
    driver.get(url);
    Thread.sleep(5000);
    driver.manage().window().maximize();
    System.out.println("Browser maximized");
    driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}

5. 数据驱动线程安全优化(可选)

将ExcelOperations实例化移到@BeforeMethod中,避免成员变量导致的线程安全问题:

public class LoginTest extends TestBase {
LoginPage loginPage;
RegistrationPage registrationPage;
ExcelOperations excel;
ExcelOperations excel2;

@BeforeMethod
public void loadClass() {
    registrationPage = new RegistrationPage();
    excel = new ExcelOperations("validLogin");
    excel2 = new ExcelOperations("invalidLogin");
}

// 其余方法保持不变
}

验证修复效果

完成上述修改后,重新运行TestNG并行测试,两个浏览器会独立执行登录流程,不会出现重复点击或无操作的情况,线程安全问题得到解决。

内容的提问来源于stack exchange,提问作者Mayank Mishra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 18:10:32