Maven Surefire 與 ArchUnit 架構測試完整指南

本報告深入探討 Maven Surefire Plugin 的運作原理、與 ArchUnit 的整合方式、常見問題排解、Gradle 對照設定,以及 CI/CD 整合範例。


目錄

  1. Maven Surefire 完整運作原理
  2. Surefire 測試發現機制
  3. Surefire 與 ArchUnit 的整合
  4. Surefire vs Failsafe 差異
  5. 常見 Surefire + ArchUnit 問題與排解
  6. Surefire 報告生成
  7. 多模組 Maven 專案中的 ArchUnit
  8. Fork 與平行執行效能調校
  9. Gradle 對照設定
  10. CI/CD 整合範例
  11. 完整 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-junit3surefire-junit4surefire-junit47surefire-junit-platformsurefire-testng),3.6.0 後合併為單一 surefire-junit-platform,大幅簡化設定。

1.4 類別載入與 Classpath

Surefire 建構測試 classpath 的順序:

  1. test-classes 目錄(target/test-classes
  2. classes 目錄(target/classes
  3. 專案所有依賴(含所有 scope:compile、runtime、test、system、provided)
  4. 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-engineArchUnitTestEngine 實作

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-testverify
失敗行為 測試失敗 → 立即終止建置 測試失敗 → 執行 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),原因:

  1. 架構測試是「單元測試」性質 — 不需要外部環境
  2. 失敗應立即停止建置 — 架構違規不應被忽略
  3. 執行速度快 — 無需 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 不執行

解決方案:

  1. 降級 Surefire 版本至 3.5.2:
<properties>
    <maven-surefire-plugin.version>3.5.2</maven-surefire-plugin.version>
</properties>
  1. 或升級至已修復的版本(3.6.0+):
<properties>
    <maven-surefire-plugin.version>3.6.0-M1</maven-surefire-plugin.version>
</properties>
  1. 暫時 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 版本不相容。

排解步驟:

  1. 確認使用 archunit-junit5(不是 archunit-core
  2. 確認 Surefire 版本 >= 2.22.0
  3. 確認 JUnit Platform 依賴完整:
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit-junit5</artifactId>
    <version>1.4.2</version>
    <scope>test</scope>
</dependency>
  1. 如果使用自訂 @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 但沒有報錯

可能原因:

  1. 測試類別不符合預設命名規則(*Test.javaTest*.java
  2. Surefire 版本太舊不支援 JUnit 5
  3. 有其他測試框架(如 TestNG)在 classpath 上導致誤判 provider
  4. 使用了 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 上的類別,這個過程:

  1. 記憶體密集 — 需要足夠 heap space
  2. CPU 密集 — 解析字節碼
  3. 共享快取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 的非標準 TestSourceFieldSource)支援有限。

// ⚠️ 使用 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