Java(Selenium)中多线程并行测试用例如何通过Apache POI并发访问同一Excel?
解决Java+Selenium并发测试时Apache POI的线程安全问题
这个问题我在搭建自动化测试框架时碰过好几次,核心原因就是Apache POI的绝大多数类(比如XSSFWorkbook、Sheet)都不是线程安全的——多个线程同时操作同一个POI实例或者同一个Excel文件,必然会抛出IO异常或者出现数据错乱。下面给你几个实用的解决方案,按优先级推荐:
1. 提前加载所有数据到线程安全的内存集合(最优方案)
这是我最推荐的方式:把Excel里的所有测试数据在测试启动阶段用单线程一次性加载到内存,之后所有并发测试线程直接从内存取数据,完全不碰POI和Excel文件。
实现步骤:
- 在测试套件初始化时(比如TestNG的
@BeforeSuite或者JUnit的@BeforeAll),用单线程读取整个Excel,解析成你需要的测试数据对象列表。 - 把列表转换成线程安全的集合,比如
CopyOnWriteArrayList或者ConcurrentHashMap(如果需要按用例ID快速查找)。 - 每个测试线程直接从内存集合中获取自己的测试数据。
代码示例:
// 全局存储测试数据,用线程安全集合 private static List<TestCaseData> allTestData; // 测试套件启动时加载数据(单线程执行) @BeforeSuite public void loadTestDataFromExcel() { try (FileInputStream fis = new FileInputStream("test_cases.xlsx"); XSSFWorkbook workbook = new XSSFWorkbook(fis)) { // 自定义方法:把Workbook解析成测试数据对象列表 List<TestCaseData> tempData = parseExcelToTestData(workbook); // 转成线程安全的CopyOnWriteArrayList,避免并发修改问题 allTestData = new CopyOnWriteArrayList<>(tempData); } catch (IOException e) { throw new RuntimeException("初始化测试数据失败", e); } } // 测试用例中直接取数据 @Test public void testLogin() { // 根据当前用例ID或者索引获取对应数据 TestCaseData data = getTestDataForCurrentCase(); // 执行Selenium测试逻辑:比如输入用户名密码 driver.findElement(By.id("username")).sendKeys(data.getUsername()); }
优势:
- 彻底避免多线程操作POI的冲突,性能最优(只做一次IO操作)。
- 测试执行时不需要等待IO,速度更快。
注意:
- 如果Excel数据量极大(比如几万条测试用例),可能会占用较多内存,但自动化测试场景下很少会有这么大的数据集。
2. 给POI操作加全局同步锁(简单但性能差)
如果不想提前加载数据(比如测试数据会动态修改),可以给所有操作POI的代码块加一个全局锁,确保同一时间只有一个线程在读取Excel。
代码示例:
// 全局锁对象,必须是静态的,确保所有线程用同一个锁 private static final Object POI_LOCK = new Object(); public TestCaseData getTestData(int caseId) { synchronized (POI_LOCK) { try (FileInputStream fis = new FileInputStream("test_cases.xlsx"); XSSFWorkbook workbook = new XSSFWorkbook(fis)) { // 根据caseId查找对应数据 return findTestDataById(workbook, caseId); } catch (IOException e) { throw new RuntimeException("读取测试数据失败", e); } } }
优势:
- 代码改动最小,不需要重构数据加载逻辑。
劣势:
- 并发测试会变成串行读取数据,严重影响执行效率,只适合并发量极低的场景。
3. 每个线程独立创建POI实例(折中方案)
让每个测试线程自己打开Excel文件、创建Workbook实例,读完就关闭。这样每个线程有独立的POI资源,不会互相干扰。
代码示例:
@Test public void testCheckout() { // 每个测试线程单独打开Excel和创建Workbook try (FileInputStream fis = new FileInputStream("test_cases.xlsx"); XSSFWorkbook workbook = new XSSFWorkbook(fis)) { // 获取当前用例对应的测试数据 TestCaseData data = parseDataForCurrentTest(workbook); // 执行测试逻辑 driver.findElement(By.id("cart-checkout")).click(); // ... } catch (IOException e) { throw new RuntimeException("读取测试数据失败", e); } }
优势:
- 不需要锁,线程之间互不影响。
劣势:
- 每个线程都要做IO操作,频繁打开关闭Excel文件,性能不如提前加载方案。
- 要注意用
try-with-resources确保文件流和Workbook被正确关闭,避免文件句柄泄漏。
额外注意事项
- 如果用TestNG做并发测试,确保你的
@DataProvider是线程安全的,最好直接返回提前加载好的内存集合。 - 尽量避免在测试执行过程中修改Excel文件,哪怕是单线程也可能导致数据不一致。
内容的提问来源于stack exchange,提问作者twistedninja
相关产品推荐
相关产品推荐

