1. 简介
大多数情况下,串行执行测试完全没问题。但在项目规模变大、测试用例增多时,串行执行就成了性能瓶颈。这时候,并行执行测试就成了提升 CI/CD 效率的利器。
本文将详细介绍如何结合 JUnit 与 Maven 的 Surefire 插件实现测试的并行化。我们会先覆盖单模块内的并行执行,再扩展到多模块项目中的并行构建与测试。内容不讲基础概念,直奔主题,适合有一定经验的开发者参考,避免踩坑。
2. Maven 依赖
要启用并行测试,需确保使用 JUnit 4.7+ 和 **Surefire 插件 2.16+**。低于此版本的组合不支持完整的并行能力。
✅ 推荐依赖配置如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
</plugin>
⚠️ 注意:
- JUnit 5 需使用
junit-platform-surefire-provider
,本文基于 JUnit 4 示例,但原理相通 - Surefire 版本过低会导致并行参数不生效,建议直接用 2.20+ 避免踩坑
Surefire 支持两种并行模式:
- ✅ 多线程模式:在同一个 JVM 内通过线程并行执行测试(内存共享,速度快)
- ✅ 进程分叉模式(Forking):启动多个 JVM 进程,隔离执行(无共享内存,更安全)
下面分别展开。
3. 并行执行测试
要启用并行,测试类无需特殊注解,只要使用 JUnit 默认的 Runner(即 ParentRunner
的子类)即可。大多数标准测试类都满足条件。
3.1 使用 parallel
参数控制并行粒度
通过 <parallel>
参数指定并行的粒度级别,这是核心配置。
支持的值包括:
粒度 | 说明 |
---|---|
methods |
每个测试方法在独立线程中执行 |
classes |
每个测试类在独立线程中执行 |
classesAndMethods |
类和方法均可并行 |
suites |
测试套件(Test Suite)并行 |
suitesAndClasses |
套件和类并行 |
suitesAndMethods |
套件和方法并行 |
all |
所有层级全部并行 |
🌰 示例:启用最彻底的并行化
<configuration>
<parallel>all</parallel>
<useUnlimitedThreads>true</useUnlimitedThreads>
</configuration>
接下来需要控制线程数量:
threadCount
:指定最大线程数useUnlimitedThreads
:启用后,线程数 = CPU 核心数 ×perCoreThreadCount
perCoreThreadCount
:是否按每核创建线程(默认true
)
🌰 示例:限制总线程数
<configuration>
<parallel>all</parallel>
<threadCount>10</threadCount>
<perCoreThreadCount>false</perCoreThreadCount>
</configuration>
3.2 细粒度线程数控制
如果想对不同层级分别控制线程数,可使用以下参数:
<threadCountSuites>2</threadCountSuites>
<threadCountClasses>2</threadCountClasses>
<threadCountMethods>6</threadCountMethods>
✅ 使用建议:
- 当
parallel=all
时,上述三个参数可精确控制各层级并发数 - 若省略某一层(如
threadCountMethods
),Surefire 会自动分配剩余线程 - 但必须保证:
threadCount >= threadCountClasses + threadCountSuites + ...
⚠️ 注意:即使使用 useUnlimitedThreads=true
,也可通过 threadCountClasses
等参数限制特定层级的并发数,避免资源耗尽。
3.3 设置超时机制
并行测试一旦卡住,可能拖垮整个构建。因此建议设置超时保护。
Surefire 提供两个超时参数:
参数 | 作用 |
---|---|
parallelTestTimeoutForcedInSeconds |
到时强制中断所有运行中的测试,丢弃队列中未执行的测试 |
parallelTestTimeoutInSeconds |
到时停止新测试执行,但允许正在运行的测试完成 |
🌰 示例:
<parallelTestTimeoutForcedInSeconds>5</parallelTestTimeoutForcedInSeconds>
<parallelTestTimeoutInSeconds>3.5</parallelTestTimeoutInSeconds>
✅ 建议:
- 使用
Forced
版本防止 CI 挂起 - 超时后测试会标记为失败,便于快速发现问题
3.4 注意事项与踩坑点
并行执行虽快,但有副作用,需特别注意:
❌ 静态方法的并发安全问题
Surefire 会在主线程中调用以下注解的方法:
@Parameters
@BeforeClass
@AfterClass
这些方法不会并行执行,但如果它们操作了共享状态(如静态变量),仍可能引发内存不一致或竞态条件。
❌ 共享状态的测试类不适用并行
例如:
- 修改系统属性(
System.setProperty
) - 操作静态缓存
- 写临时文件且路径固定
✅ 建议:
- 尽量让测试无状态
- 使用随机端口、临时目录隔离资源
- 必要时使用
synchronized
或@NotThreadSafe
标记
4. 多模块项目的并行构建
前面讲的是单模块内测试的并行。但在多模块项目中,默认情况下,Maven 会串行构建模块,导致测试也无法并行。
解决方案:使用 Maven 的 -T
参数启用并行构建。
🌰 示例:
# 使用 4 个线程构建
mvn -T 4 clean test
# 按每核 1 个线程(推荐,可移植)
mvn -T 1C clean test
✅ 效果:
- 多个模块的构建和测试可同时进行
- 显著缩短整体 CI 时间
- 尤其适合微服务或模块化单体项目
⚠️ 注意:
-T
控制的是构建线程,不影响模块内部的测试并行(那是 Surefire 的事)- 建议与 Surefire 的并行配置叠加使用,实现“内外双重并行”
5. JVM 分叉(Forking)模式
前面的并行是基于线程的,所有测试运行在同一个 JVM 中,共享内存空间。
虽然高效,但也带来了风险:
- 类加载器污染
- 静态变量残留
- 第三方库状态未清理
解决方案:使用 Forking 模式,每个测试运行在独立的 JVM 进程中。
启用 Forking
通过 forkCount
参数指定分叉的 JVM 数量:
<forkCount>3</forkCount>
- 默认值为
1
,即每个模块启动一个新 JVM 执行所有测试 - 设为
0
表示禁用分叉(所有测试在主线程 JVM 执行)
支持动态计算:
<forkCount>2.5C</forkCount>
🌰 含义:每 CPU 核心创建 2.5 个 JVM。例如 2 核机器 → 最多 5 个分叉。
复用 Forked JVM
默认情况下,Surefire 会复用创建的 JVM 进程(类似线程池),以减少启动开销。
如需每个测试类后销毁 JVM(更彻底隔离),可关闭复用:
<forkCount>2</forkCount>
<reuseForks>false</reuseForks>
✅ 使用建议:
reuseForks=true
:性能优先,适合稳定测试reuseForks=false
:隔离优先,适合有状态或不稳定测试
6. 总结
本文系统梳理了 Maven + JUnit 实现测试并行化的完整方案:
- ✅ 线程级并行:通过
parallel
+threadCount
实现,速度快但需注意并发安全 - ✅ 进程级并行:通过
forkCount
实现,隔离性好,适合复杂场景 - ✅ 多模块并行:使用
-T
参数开启模块间并行构建 - ✅ 超时保护:避免测试卡死,提升 CI 稳定性
📌 最佳实践建议:
- 先从
parallel=classes
+forkCount=2C
开始尝试 - 避免测试间共享状态
- 结合 CI 环境合理设置线程/进程数,避免资源争抢
文中所有示例代码已整理至 GitHub: