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 稳定性

📌 最佳实践建议:

  1. 先从 parallel=classes + forkCount=2C 开始尝试
  2. 避免测试间共享状态
  3. 结合 CI 环境合理设置线程/进程数,避免资源争抢

文中所有示例代码已整理至 GitHub:

👉 https://github.com/tech-tutorial/parallel-tests-junit


原始标题:Running JUnit Tests in Parallel with Maven