使用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
相关产品推荐
相关产品推荐

