Maven Surefire 與 ArchUnit 架構測試完整指南
本報告深入探討 Maven Surefire Plugin 的運作原理、與 ArchUnit 的整合方式、常見問題排解、Gradle 對照設定,以及 CI/CD 整合範例。
目錄
- Maven Surefire 完整運作原理
- Surefire 測試發現機制
- Surefire 與 ArchUnit 的整合
- Surefire vs Failsafe 差異
- 常見 Surefire + ArchUnit 問題與排解
- Surefire 報告生成
- 多模組 Maven 專案中的 ArchUnit
- Fork 與平行執行效能調校
- Gradle 對照設定
- CI/CD 整合範例
- 完整 pom.xml 範例
1. Maven Surefire 完整運作原理
1.1 Surefire 是什麼
Maven Surefire Plugin 是 Apache Maven 的核心測試插件,負責在 test 階段執行單元測試。它是 Maven 生態系中最基礎且最重要的插件之一。
三大核心組件:
| 插件 | 用途 | 生命週期綁定 |
|---|---|---|
maven-surefire-plugin |
執行單元測試 | test |
maven-failsafe-plugin |
執行整合測試 | integration-test / verify |
maven-surefire-report-plugin |
產生 HTML 測試報告 | site |
1.2 運作流程
mvn test
↓
Maven 生命週期 → test 階段
↓
Surefire Plugin 啟動
↓
1. 掃描 src/test/java 下的測試類別(根據命名規則)
2. 構建測試 classpath(依序加入)
- test-classes 目錄
- classes 目錄
- 所有 scope 的專案依賴
- additionalClasspathElements
3. 選擇測試 Provider(根據 classpath 上的測試框架)
4. Fork JVM 執行測試(預設 forkCount=1)
5. 收集測試結果,寫入 target/surefire-reports/
6. 與 Maven 主進程通報結果
1.3 測試 Provider 選擇(Surefire 3.6.0+ 統一機制)
自 Surefire 3.6.0 起,所有測試框架都透過 JUnit Platform 統一執行:
if JUnit 5 Platform artifacts 存在
→ 使用 surefire-junit-platform provider
→ 自動載入 Jupiter Engine
if JUnit 4.12+ 存在
→ 使用 surefire-junit-platform provider
→ 自動載入 Vintage Engine
if TestNG 6.14.3+ 存在
→ 使用 surefire-junit-platform provider
→ 自動載入 TestNG JUnit Platform Engine
重要變更(3.6.0): 舊版有多個 provider(surefire-junit3、surefire-junit4、surefire-junit47、surefire-junit-platform、surefire-testng),3.6.0 後合併為單一 surefire-junit-platform,大幅簡化設定。
1.4 類別載入與 Classpath
Surefire 建構測試 classpath 的順序:
test-classes目錄(target/test-classes)classes目錄(target/classes)- 專案所有依賴(含所有 scope:compile、runtime、test、system、provided)
additionalClasspathElements指定的額外路徑
Surefire 使用 manifest-only JAR 技術將 classpath 傳遞給 forked JVM,避免 Windows 上 classpath 過長的問題(surefire.useManifestOnlyJar 預設為 true)。
2. Surefire 測試發現機制
2.1 預設命名規則
Surefire 自動包含以下命名模式的測試類別:
**/Test*.java → 以 Test 開頭
**/*Test.java → 以 Test 結尾
**/*Tests.java → 以 Tests 結尾
**/*TestCase.java → 以 TestCase 結尾
2.2 自訂 include/exclude
<configuration>
<includes>
<include>**/Architecture*Test.java</include>
<include>**/*ArchTest.java</include>
</includes>
<excludes>
<exclude>**/SlowIntegration*Test.java</exclude>
</excludes>
</configuration>
支援正則表達式語法(以 %regex[] 包裹):
<includes>
<include>%regex[.*(Architecture|Layer).*Test.*]</include>
</includes>
2.3 JUnit 5 測試發現
JUnit 5 使用 TestEngine 機制發現測試。ArchUnit 有專屬的 ArchUnitTestEngine(ID 為 archunit),Surefire 會自動透過 JUnit Platform Launcher 發現並執行它。
3. Surefire 與 ArchUnit 的整合
3.1 基本整合設定
ArchUnit 以 JUnit 5 TestEngine 方式整合,不需要額外的 Maven 插件。只需正確設定依賴:
<!-- ArchUnit 核心(含 JUnit 5 支援) -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
archunit-junit5 會 transitively 引入:
- archunit-core:核心分析引擎
- archunit-junit5-api:@AnalyzeClasses、@ArchTest 等註解
- archunit-junit5-engine:ArchUnitTestEngine 實作
3.2 基本 ArchUnit 測試寫法
@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {
@ArchTest
static final ArchRule layered_architecture = layeredArchitecture()
.consideringAllDependencies()
.layer("Controller").definedBy("..controller..")
.layer("Service").definedBy("..service..")
.layer("Repository").definedBy("..repository..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
@ArchTest
static final ArchRule no_cycles = slices()
.matching("com.example.myapp.(*)..")
.should().beFreeOfCycles();
@ArchTest
static final ArchRule naming_conventions = classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service");
}
3.3 Surefire 與 ArchUnit 整合的運作原理
mvn test
↓
Surefire 發現 ArchitectureTest 類別(符合 *Test.java 命名)
↓
Surefire 使用 JUnit Platform Provider
↓
JUnit Platform Launcher 發現 ArchUnitTestEngine
↓
ArchUnitTestEngine 執行 @AnalyzeClasses 指定的 packages
↓
逐一檢查 @ArchTest 標註的規則
↓
結果寫入 target/surefire-reports/TEST-ArchitectureTest.xml
3.4 使用 ArchTests 群組化規則
對於大型專案,可以使用 ArchTests 將規則模組化:
// 定義規則集合
public class NamingRules {
@ArchTest
static final ArchRule services_should_end_with_Service = classes()
.that().resideInAPackage("..service..")
.should().haveSimpleNameEndingWith("Service");
@ArchTest
static final ArchRule repositories_should_end_with_Repository = classes()
.that().resideInAPackage("..repository..")
.should().haveSimpleNameEndingWith("Repository");
}
// 在主測試類別中引用
@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {
@ArchTest
static final ArchTests naming_rules = ArchTests.in(NamingRules.class);
@ArchTest
static final ArchTests dependency_rules = ArchTests.in(DependencyRules.class);
}
3.5 使用 ArchUnit 從共用庫執行規則
如果要將 ArchUnit 規則打包為共用 JAR,在其他專案中執行:
方式一:使用 dependenciesToScan
<!-- 在使用端專案的 pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<dependenciesToScan>
<dependency>com.example:shared-archunit-rules</dependency>
</dependenciesToScan>
</configuration>
</plugin>
方式二:使用 ArchTests.in() 引用
@AnalyzeClasses(packages = "com.example.myapp")
public class LocalArchitectureTest {
@ArchTest
static final ArchTests shared_rules = ArchTests.in(SharedArchitectureRules.class);
}
4. Surefire vs Failsafe 差異
4.1 核心差異比較
| 特性 | Surefire | Failsafe |
|---|---|---|
| 用途 | 單元測試 | 整合測試 |
| 生命週期 | test |
integration-test → verify |
| 失敗行為 | 測試失敗 → 立即終止建置 | 測試失敗 → 執行 post-integration-test 後才失敗 |
| 預設命名 | *Test.java, Test*.java |
*IT.java, IT*.java, *ITCase.java |
| 報告目錄 | target/surefire-reports |
target/failsafe-reports |
| 清理保證 | 無(build 直接中斷) | 有(可確保 post-integration-test 執行) |
4.2 ArchUnit 應使用 Surefire
ArchUnit 架構測試應使用 Surefire(而非 Failsafe),原因:
- 架構測試是「單元測試」性質 — 不需要外部環境
- 失敗應立即停止建置 — 架構違規不應被忽略
- 執行速度快 — 無需 setup/teardown 環境
4.3 何時用 Failsafe
如果 ArchUnit 規則需要掃描已部署的 WAR/JAR(整合測試性質),可考慮使用 Failsafe:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<includes>
<include>**/*ArchIT.java</include>
</includes>
</configuration>
</plugin>
5. 常見 Surefire + ArchUnit 問題與排解
5.1 ⚠️ Surefire 3.5.3+ @ArchTest 欄位型規則不執行(已知問題)
問題描述: Surefire 3.5.3 版本存在已知回歸問題(TNG/ArchUnit#1442),導致以**欄位(field)**宣告的 @ArchTest 規則不被執行。mvn test 顯示 Tests run: 0,build 卻是綠色。
問題程式碼:
@ArchTest
static final ArchRule my_rule = classes()...; // ← 欄位型,在 Surefire 3.5.3 不執行
解決方案:
- 降級 Surefire 版本至 3.5.2:
<properties>
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
</properties>
- 或升級至已修復的版本(3.6.0+):
<properties>
<maven-surefire-plugin.version>3.6.0-M1</maven-surefire-plugin.version>
</properties>
- 暫時 workaround — 將規則改為方法型:
// 欄位型(可能有問題)
@ArchTest
static final ArchRule my_rule = classes()...;
// 方法型(較安全的 workaround)
@ArchTest
static void my_rule(JavaClasses classes) {
classes()...check(classes);
}
診斷方式: 檢查 Tests run 數量,不是只看 build 是否綠色。Tests run: 0 就是沒有執行。
5.2 TestEngine with ID 'archunit' failed to discover tests
原因: 通常是依賴衝突或 ArchUnit 版本不相容。
排解步驟:
- 確認使用
archunit-junit5(不是archunit-core) - 確認 Surefire 版本 >= 2.22.0
- 確認 JUnit Platform 依賴完整:
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
- 如果使用自訂
@Tag過濾,改用@ArchTag:
// ❌ 錯誤:ArchUnit 不認識 JUnit 5 的 @Tag
@Tag("architecture")
@ArchTest
static final ArchRule rule = ...;
// ✅ 正確:使用 ArchUnit 自己的 @ArchTag
@ArchTag("architecture")
@ArchTest
static final ArchRule rule = ...;
5.3 ArchUnit 測試執行緩慢
常見原因與對策:
# archunit.properties(放在 src/test/resources/)
# 1. 停用從 classpath 解析缺失類別(大幅提升速度)
resolveMissingDependenciesFromClassPath=false
# 2. 限制解析迭代次數
# 預設值通常合理,但可以調低
resolutionIterations=1
# 3. 只解析特定套件
resolvePackages=com.example.myapp.**
使用 @AnalyzeClasses 精確指定掃描範圍:
@AnalyzeClasses(
packages = "com.example.myapp",
importOptions = {
ImportOption.DoNotIncludeTests.class,
ImportOption.DoNotIncludeJars.class
}
)
public class ArchitectureTest { ... }
使用 CacheMode 控制快取行為:
@AnalyzeClasses(
packages = "com.example.myapp",
cacheMode = CacheMode.PER_CLASS // 每個測試類別獨立快取
)
public class ArchitectureTest { ... }
5.4 Tests run: 0 但沒有報錯
可能原因:
- 測試類別不符合預設命名規則(
*Test.java、Test*.java) - Surefire 版本太舊不支援 JUnit 5
- 有其他測試框架(如 TestNG)在 classpath 上導致誤判 provider
- 使用了
includes過濾但未包含 ArchUnit 測試類別
排解:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- 確保 ArchUnit 測試類別被包含 -->
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
<include>**/*ArchTest.java</include>
</includes>
</configuration>
</plugin>
5.5 Classpath 問題:類別找不到
Surefire 的 additionalClasspathElements:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>${project.basedir}/lib/my-custom.jar</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</plugin>
移除特定依賴(classpath 衝突時):
<configuration>
<classpathDependencyExcludes>
<classpathDependencyExcludes>org某些衝突的groupId:artifactId</classpathDependencyExcludes>
</classpathDependencyExcludes>
<classpathDependencyScopeExclude>provided</classpathDependencyScopeExclude>
</configuration>
5.6 ArchUnit @ArchTest with groups/tags 不執行
問題: 使用 Surefire 的 groups 參數過濾時,ArchUnit 測試不被執行。
原因: ArchUnit 有自己獨立的 TestEngine,不直接使用 JUnit Jupiter 的 @Tag。
解決: 使用 ArchUnit 自己的 @ArchTag:
@ArchTag("architecture")
@AnalyzeClasses(packages = "com.example.myapp")
public class ArchitectureTest {
@ArchTag("layered")
@ArchTest
static final ArchRule layered = layeredArchitecture()...;
}
Surefire 設定:
<configuration>
<groups>architecture</groups>
</configuration>
6. Surefire 報告生成
6.1 XML 報告(自動產生)
Surefire 在執行測試時自動產生 XML 報告:
target/surefire-reports/
├── TEST-com.example.ArchitectureTest.xml
├── TEST-com.example.LayerTest.xml
├── com.example.ArchitectureTest.txt
├── com.example.LayerTest.txt
└── ...
6.2 HTML 報告(maven-surefire-report-plugin)
pom.xml 設定:
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</reporting>
產生報告:
# 獨立產生(不執行測試)
mvn surefire-report:report
# 或作為 site 的一部分
mvn test site
# 產生報告但仍顯示失敗
mvn surefire-report:report -DshowSuccess=false
報告輸出位置:
target/site/surefire-report.html
設定報告選項:
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- 只顯示失敗的測試 -->
<showSuccess>false</showSuccess>
<!-- 自訂報告檔名 -->
<outputName>Architecture-Test-Report</outputName>
<!-- 產生來源交叉參照 -->
<linkXRef>true</linkXRef>
<inputXRefDirectory>${project.build.sourceDirectory}</inputXRefDirectory>
</configuration>
</plugin>
</plugins>
</reporting>
6.3 報告範例輸出
XML 格式(TEST-ArchitectureTest.xml):
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.example.ArchitectureTest"
tests="3" errors="1" failures="0" skipped="0"
time="2.456">
<testcase name="layered_architecture"
classname="com.example.ArchitectureTest"
time="1.234"/>
<testcase name="no_cycles"
classname="com.example.ArchitectureTest"
time="0.567"/>
<testcase name="naming_conventions"
classname="com.example.ArchitectureTest"
time="0.655">
<failure message="Architecture Violation">
<![CDATA[Architecture Violation [Priority: MEDIUM]
Rule 'classes that reside in a package '..service..' should have simple name ending with 'Service'' was violated (1 times):
Class <com.example.myapp.service.MyHelper> does not have a simple name ending with 'Service' in (MyHelper.java:0)]]>
</failure>
</testcase>
</testsuite>
TXT 格式(com.example.ArchitectureTest.txt):
-------------------------------------------------------------------------------
Test set: com.example.ArchitectureTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 2.456 s <<< ERROR! -- in com.example.ArchitectureTest
6.4 @DisplayName 支援
Surefire 3.0.0-M4+ 支援 JUnit 5 的 @DisplayName,可在報告中顯示更可讀的名稱:
<configuration>
<statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<disable>false</disable>
<version>3.0.2</version>
<usePhrasedTestSuiteClassName>true</usePhrasedTestSuiteClassName>
<usePhrasedTestCaseClassName>true</usePhrasedTestCaseClassName>
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
</statelessTestsetReporter>
<consoleOutputReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5ConsoleOutputReporter">
<disable>false</disable>
<encoding>UTF-8</encoding>
</consoleOutputReporter>
</configuration>
7. 多模組 Maven 專案中的 ArchUnit
7.1 架構概述
parent-project/
├── pom.xml ← 父 POM(聚合器)
├── module-common/
│ └── src/test/java/ ← 共用規則
├── module-api/
│ └── src/test/java/ ← API 模組測試
├── module-service/
│ └── src/test/java/ ← Service 模組測試
└── module-archunit-tests/ ← 專門放架構測試的模組
└── src/test/java/
7.2 方案一:每個子模組獨立執行 ArchUnit
每個子模組各自包含 ArchUnit 依賴和測試:
<!-- 每個子模組的 pom.xml -->
<dependencies>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
7.3 方案二:集中式架構測試模組
建立專門的 module-archunit-tests 模組,掃描所有子模組的類別:
<!-- module-archunit-tests/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-api</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>module-service</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.2</version>
<scope>test</scope>
</dependency>
</dependencies>
@AnalyzeClasses(packages = "com.example")
public class CrossModuleArchitectureTest {
@ArchTest
static final ArchRule modules_should_not_have_cycles = slices()
.matching("com.example.(*)..")
.should().beFreeOfCycles();
@ArchTest
static final ArchRule api_should_not_depend_on_service = noClasses()
.that().resideInAPackage("..api..")
.should().dependOnClassesThat()
.resideInAPackage("..service..");
}
7.4 方案三:使用 dependenciesToScan 共用規則
將共用的 ArchUnit 規則打包成 JAR,在各模組中透過 dependenciesToScan 執行:
<!-- 共用規則模組 -->
<groupId>com.example</groupId>
<artifactId>archunit-rules-shared</artifactId>
<!-- 使用端的 pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<dependenciesToScan>
<dependency>com.example:archunit-rules-shared</dependency>
</dependenciesToScan>
</configuration>
</plugin>
7.5 聚合報告
方法一:在父 POM 使用 surefire-report-plugin aggregate:
<!-- 父 POM 的 reporting 區段 -->
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.5.2</version>
<inherited>false</inherited>
<configuration>
<aggregate>true</aggregate>
</configuration>
</plugin>
</plugins>
</reporting>
# 注意:需要執行兩次才能得到正確的聚合報告
mvn clean test
mvn surefire-report:report -Daggregate=true
方法二:使用 Maven Flatten Plugin + CI 步驟:
# CI 中先執行測試,再產生報告
- run: mvn test --batch-mode
- run: mvn surefire-report:report -Daggregate=true
8. Fork 與平行執行效能調校
8.1 Fork 機制
Surefire 透過 Fork JVM 執行測試,與 Maven 主進程隔離。關鍵參數:
| 參數 | 預設值 | 說明 |
|---|---|---|
forkCount |
1 |
Fork 的 JVM 數量。支援 1.5C 語法(乘以 CPU 核心數) |
reuseForks |
true |
是否重用 forked JVM |
forkedProcessTimeoutInSeconds |
無 | Forked JVM 超時時間 |
8.2 平行執行策略
策略一:Process 級平行(forkCount > 1)
<configuration>
<forkCount>2</forkCount> <!-- 同時執行 2 個 JVM -->
<reuseForks>true</reuseForks> <!-- 重用 JVM 以減少啟動開銷 -->
<argLine>-Xmx1024m -Xms512m</argLine>
</configuration>
策略二:Thread 級平行(parallel 參數)
<configuration>
<parallel>methods</parallel> <!-- 方法級平行 -->
<threadCount>4</threadCount> <!-- 每個 fork 4 個執行緒 -->
<perCoreThreadCount>true</perCoreThreadCount> <!-- 按 CPU 核心數自動調整 -->
</configuration>
策略三:組合使用
<configuration>
<forkCount>2</forkCount> <!-- 2 個 JVM -->
<reuseForks>true</reuseForks>
<parallel>methods</parallel> <!-- 方法級平行 -->
<threadCount>4</threadCount> <!-- 每個 JVM 4 執行緒 -->
</configuration>
8.3 ArchUnit 特殊考量
ArchUnit 的 ClassFileImporter 會掃描 classpath 上的類別,這個過程:
- 記憶體密集 — 需要足夠 heap space
- CPU 密集 — 解析字節碼
- 共享快取 —
CacheMode.PER_LOCATION(預設)在多 fork 間共享快取
推薦的效能設定:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- ArchUnit 建議較大的 heap -->
<argLine>-Xmx2048m -Xms1024m</argLine>
<!-- 單 fork 重用(ArchUnit 快取有效) -->
<forkCount>1</forkCount>
<reuseForks>true</reuseForks>
<!-- 如需平行,使用 methods 級 -->
<parallel>methods</parallel>
<threadCount>2</threadCount>
<!-- 超時保護 -->
<parallelTestsTimeoutInSeconds>300</parallelTestsTimeoutInSeconds>
<parallelTestsTimeoutForcedInSeconds>600</parallelTestsTimeoutForcedInSeconds>
</configuration>
</plugin>
8.4 超時控制
<configuration>
<!-- 超時後列印已執行的測試 -->
<parallelTestsTimeoutInSeconds>300</parallelTestsTimeoutInSeconds>
<!-- 強制終止所有執行緒 -->
<parallelTestsTimeoutForcedInSeconds>600</parallelTestsTimeoutForcedInSeconds>
</configuration>
8.5 失敗後跳過剩餘測試
<configuration>
<!-- 發現 1 個失敗後跳過剩餘測試 -->
<skipAfterFailureCount>1</skipAfterFailureCount>
</configuration>
命令列方式:
mvn test -Dsurefire.skipAfterFailureCount=1
9. Gradle 對照設定
9.1 基本 Gradle ArchUnit 設定
// build.gradle
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
9.2 Gradle 與 Maven Surefire 對照表
| Maven Surefire | Gradle Test | 說明 |
|---|---|---|
<forkCount>2</forkCount> |
maxParallelForks = 2 |
平行 JVM 數 |
<parallel>methods</parallel> |
testsOutOfOrder = true |
方法級平行 |
<threadCount>4</threadCount> |
forkEvery = 4 |
每 N 個測試 fork 一次 |
<argLine>-Xmx2g</argLine> |
maxHeapSize = '2g' |
JVM heap 大小 |
<includes> |
include 'pattern' |
包含測試 |
<excludes> |
exclude 'pattern' |
排除測試 |
<skipTests>true</skipTests> |
enabled = false |
跳過測試 |
<includesFile> |
include 'pattern' |
從檔案讀取包含規則 |
<excludesFile> |
exclude 'pattern' |
從檔案讀取排除規則 |
9.3 完整 Gradle 效能調校
test {
useJUnitPlatform()
// 平行執行設定
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
// JVM 設定
minHeapSize = '512m'
maxHeapSize = '2g'
jvmArgs = [
'-XX:+UseG1GC',
'-XX:MaxGCPauseMillis=200',
'--add-opens', 'java.base/java.lang=ALL-UNNAMED'
]
// 測試發現
include '**/*Test.class'
exclude '**/*SlowTest.class'
// 報告
testLogging {
events 'passed', 'skipped', 'failed'
exceptionFormat 'full'
showStandardStreams = true
}
// XML 報告(CI 用)
reports {
junitXml.required = true
html.required = true
}
// 失敗後繼續
failFast = false
}
9.4 Gradle ArchUnit 特殊注意事項
Gradle 已知問題: Gradle 的 JUnit Platform 篩選器對 ArchUnit 的非標準 TestSource(FieldSource)支援有限。
// ⚠️ 使用 filter 可能排除 ArchUnit 測試
test {
useJUnitPlatform()
filter {
excludeTestsMatching '*IT' // 可能意外排除 ArchUnit 測試
}
}
// ✅ 建議:不使用 filter,或使用 @ArchTag 管理
test {
useJUnitPlatform()
// 用 @ArchTag 在程式碼中管理過濾
}
9.5 Gradle 整合測試設定
sourceSets {
integrationTest {
java.srcDir 'src/integrationTest/java'
resources.srcDir 'src/integrationTest/resources'
compileClasspath += sourceSets.main.output + sourceSets.test.output
runtimeClasspath += sourceSets.main.output + sourceSets.test.output
}
}
configures {
integrationTestImplementation.extendsFrom testImplementation
integrationTestRuntimeOnly.extendsFrom testRuntimeOnly
}
task integrationTest(type: Test) {
description = 'Runs integration tests.'
group = 'verification'
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
useJUnitPlatform()
shouldRunAfter test
}
check.dependsOn integrationTest
10. CI/CD 整合範例
10.1 GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build and Test
run: mvn clean test --batch-mode
- name: Publish Test Report
if: success() || failure()
uses: scacap/action-surefire-report@v1
with:
report_paths: '**/surefire-reports/TEST-*.xml'
github_token: ${{ secrets.GITHUB_TOKEN }}
fail_if_no_tests: false
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: surefire-reports
path: '**/surefire-reports/'
retention-days: 30
10.2 GitHub Actions(Gradle 版本)
name: CI (Gradle)
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and Test
run: ./gradlew test
- name: Publish Test Report
if: success() || failure()
uses: scacap/action-surefire-report@v1
with:
# Gradle 報告路徑不同
report_paths: '**/build/test-results/test/TEST-*.xml'
github_token: ${{ secrets.GITHUB_TOKEN }}
10.3 Jenkins Pipeline
// Jenkinsfile
pipeline {
agent any
stages {
stage('Build & Test') {
steps {
sh 'mvn clean test --batch-mode -Dmaven.test.failure.ignore=true'
}
post {
always {
// 收集 JUnit XML 報告
junit '**/surefire-reports/TEST-*.xml'
// 產生 HTML 報告
sh 'mvn surefire-report:report'
// 發布 HTML 報告
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target/site',
reportFiles: 'surefire-report.html',
reportName: 'Architecture Test Report'
])
}
}
}
}
}
10.4 Azure DevOps (yaml)
# azure-pipelines.yml
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Maven@3
displayName: 'Build and Test'
inputs:
mavenPomFile: 'pom.xml'
goals: 'clean test'
options: '--batch-mode'
publishJUnitResults: true
testResultsFiles: '**/surefire-reports/TEST-*.xml'
testResultsFormat: 'JUnit'
- task: PublishBuildArtifacts@1
displayName: 'Publish Test Reports'
inputs:
pathToPublish: '$(Build.SourcesDirectory)/target/site'
artifactName: 'test-reports'
10.5 GitLab CI
# .gitlab-ci.yml
stages:
- test
unit-tests:
stage: test
image: maven:3.9-eclipse-temurin-17
script:
- mvn clean test --batch-mode
artifacts:
when: always
reports:
junit:
- "**/surefire-reports/TEST-*.xml"
paths:
- "target/surefire-reports/"
- "target/site/surefire-report.html"
expire_in: 30 days
11. 完整 pom.xml 範例
11.1 基本 ArchUnit 專案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 版本控制 -->
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
<archunit.version>1.4.2</archunit.version>
<junit.version>5.10.2</junit.version>
</properties>
<dependencies>
<!-- ArchUnit JUnit 5 整合 -->
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Surefire:執行單元測試 + ArchUnit -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<!-- 測試發現 -->
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<!-- JVM 設定(ArchUnit 需要較大 heap) -->
<argLine>-Xmx2048m -Xms512m</argLine>
<!-- Fork 設定 -->
<forkCount>1</forkCount>
<reuseForks>true</reuseForks>
<!-- 平行執行(可選) -->
<!-- <parallel>methods</parallel> -->
<!-- <threadCount>2</threadCount> -->
<!-- 超時保護 -->
<parallelTestsTimeoutInSeconds>600</parallelTestsTimeoutInSeconds>
<!-- 失敗後跳過 -->
<!-- <skipAfterFailureCount>1</skipAfterFailureCount> -->
<!-- JUnit Platform 設定 -->
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled = false
</configurationParameters>
</properties>
</configuration>
</plugin>
<!-- Surefire Report:產生 HTML 報告 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
</plugin>
<!-- Maven Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
<!-- 報告設定(mvn site 用) -->
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<showSuccess>true</showSuccess>
<linkXRef>true</linkXRef>
</configuration>
</plugin>
</plugins>
</reporting>
<!-- Profile:只跑架構測試 -->
<profiles>
<profile>
<id>arch-unit-only</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<includes>
<include>**/*ArchitectureTest.java</include>
<include>**/*ArchTest.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!-- Profile:慢速測試分離 -->
<profile>
<id>skip-slow-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<excludes>
<exclude>**/*SlowTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
11.2 多模組父 POM 範例
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>module-api</module>
<module>module-service</module>
<module>module-archunit-tests</module>
</modules>
<properties>
<maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
<archunit.version>1.4.2</archunit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>-Xmx2048m</argLine>
<forkCount>1</forkCount>
<reuseForks>true</reuseForks>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<inherited>false</inherited>
<configuration>
<aggregate>true</aggregate>
</configuration>
</plugin>
</plugins>
</reporting>
</project>
附錄 A:Surefire 常用命令速查
# 執行所有測試
mvn test
# 只執行特定測試類別
mvn test -Dtest=ArchitectureTest
# 只執行特定方法
mvn test -Dtest=ArchitectureTest#layered_architecture
# 跳過測試
mvn test -DskipTests
# 跳過測試(連編譯也跳過)
mvn test -Dmaven.test.skip=true
# 忽略測試失敗繼續建置
mvn test -Dmaven.test.failure.ignore=true
# 只跑架構測試(使用 profile)
mvn test -P arch-unit-only
# 產生 HTML 報告
mvn surefire-report:report
# 產生聚合報告(多模組)
mvn surefire-report:report -Daggregate=true
# 清除後重跑
mvn clean test
# Debug 模式
mvn test -Dsurefire.useFile=false
# 強制使用特定 fork 數
mvn test -DforkCount=2 -DreuseForks=true
附錄 B:ArchUnit 版本相容性
| ArchUnit | JUnit | Surefire | Java |
|---|---|---|---|
| 1.4.x | 5.10+ | 3.2+ | 11+ |
| 1.3.x | 5.10+ | 3.2+ | 11+ |
| 1.2.x | 5.9+ | 3.0+ | 11+ |
| 1.1.x | 5.8+ | 2.22+ | 8+ |
| 1.0.x | 5.7+ | 2.22+ | 8+ |
建議: 使用 Surefire 3.5.2(穩定版)搭配 ArchUnit 1.4.2 和 JUnit 5.10.2。
報告建立日期:2026-08-23 資料來源:Apache Maven Surefire 官方文件、ArchUnit 官方文件、GitHub Issues、Stack Overflow