返回首頁

[Java] 建置工具與測試

Java 系列收尾。語法、OOP、集合、stream 都寫完之後,要讓專案真正跑得起來、發得出去、有人能維護,靠的就是這一層:建置工具與測試。純 javac + java 寫小程式還行,正式專案得有工具管依賴、編譯、打包、測試。Java 主流兩套:Maven 與 Gradle。

Maven 還是 Gradle?

MavenGradle
設定檔pom.xml(XML)build.gradle / build.gradle.kts(Groovy / Kotlin)
學習曲線直觀但囉嗦強大但較難
速度普通快(增量編譯、daemon)
適合標準專案、團隊熟悉度高大型 / Android / 自定建置流程

Spring Boot 專案兩者都能用,Android 專案則主要使用 Gradle。先跟團隊或既有專案的選擇走;會其中一個,再讀另一個的設定檔就比較有方向。

Maven 標準目錄

路徑用途
pom.xml專案與依賴設定
src/main/java/com/example/App.java應用程式
src/main/resources/application.properties隨應用程式打包的資源
src/test/java/com/example/AppTest.java測試

src/main/java 放程式、src/main/resources 放 properties 等資源、src/test/java 放測試。約定優於設定。

pom.xml 最小範例

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-app</artifactId>
    <version>0.1.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

groupId(組織)、artifactId(專案)、version 三項組成一個 GAV 座標,Maven Central 上每個 library 都有自己的 GAV。

這裡固定使用 Java 21 與 JUnit 5.10.0,不代表它們是最新版。release 同時限制語法、class 檔版本與可用的 Java API;明確指定 Surefire 版本,則避免舊版 Maven 預設測試插件沒有發現 Jupiter 測試,卻讓人以為測試已跑過。

Maven 常用指令

mvn compile              # 編譯
mvn test                 # 跑測試
mvn package              # 打成 jar 放在 target/
mvn install              # 裝到本機 repo(~/.m2)
mvn clean                # 清掉 target/
mvn clean install        # 全部重來
mvn dependency:tree      # 看依賴樹
mvn -DskipTests package  # 跳過測試打包

mvn 會自動把依賴下載到本機的 ~/.m2/repository/。

scope:依賴的可見範圍

scope編譯測試執行範例
compile(預設)✓✓✓一般 library
provided✓✓✗servlet-api(容器提供)
runtime✗✓✓JDBC driver
test✗✓✗JUnit

Gradle 最小範例(Kotlin DSL)

// build.gradle.kts
plugins {
    java
    application
}

group = "com.example"
version = "0.1.0"

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(platform("org.junit:junit-bom:5.10.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}

application {
    mainClass.set("com.example.App")
}

常用指令:

./gradlew build          # 編譯 + 測試 + 打包
./gradlew test
./gradlew run
./gradlew clean
./gradlew dependencies   # 依賴樹

./gradlew 是 Gradle Wrapper,連同 wrapper 設定一起 commit,讓大家使用專案指定的 Gradle 版本;Windows 用 gradlew.bat。第一次執行會下載該版本的 Gradle,仍需網路與合適的 JDK。這份範例要求 JDK 21;Wrapper 本身不會替你安裝 JDK,toolchain 的自動下載需要另外設定。

JUnit 5 基礎

以下沿用 JUnit 5 / Jupiter 的 API。不少舊專案還在用 JUnit 4,語法不一樣,要分清楚。

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    Calculator calc;

    @BeforeEach
    void setUp() {
        calc = new Calculator();
    }

    @AfterEach
    void tearDown() { /* ... */ }

    @Test
    @DisplayName("加法:兩個正整數")
    void addsTwoPositives() {
        assertEquals(5, calc.add(2, 3));
    }

    @Test
    void throwsOnDivByZero() {
        assertThrows(ArithmeticException.class, () -> calc.div(1, 0));
    }

    @Test
    @Disabled("修好之後再開")
    void notReadyYet() { /* 待實作 */ }
}

這裡假設已有 Calculator,提供整數 add 與 div 方法;其他片段中的 ... 同樣表示省略的專案程式碼,不是可直接編譯的 Java 語法。

生命週期 annotation

何時
@BeforeAll整個 class 跑前一次(預設生命週期下須 static)
@BeforeEach每個 @Test 前
@AfterEach每個 @Test 後
@AfterAll整個 class 跑完一次(預設生命週期下須 static)

若類別標註 @TestInstance(TestInstance.Lifecycle.PER_CLASS),JUnit 會共用該類別的測試實例,@BeforeAll/@AfterAll 就能使用非 static 方法;同時也要留意測試之間共享的可變狀態。

assertion

assertEquals(expected, actual);
assertEquals(expected, actual, "錯誤訊息");
assertEquals(0.1 + 0.2, 0.3, 1e-9);          // 浮點要給 delta

assertTrue(cond);
assertFalse(cond);
assertNull(x);
assertNotNull(x);
assertSame(a, b);                              // 參考相等
assertArrayEquals(arr1, arr2);

assertThrows(IllegalArgumentException.class, () -> doSth());
assertDoesNotThrow(() -> doSth());

// 一次斷言多個
assertAll(
    () -> assertEquals(1, x),
    () -> assertEquals(2, y)
);

參數化測試

除了前面的 Jupiter API,這些片段還需要 org.junit.jupiter.params.ParameterizedTest、org.junit.jupiter.params.provider.* 與 java.util.stream.Stream 的 import。

@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void allPositive(int n) {
    assertTrue(n > 0);
}

@ParameterizedTest
@CsvSource({
    "2, 3, 5",
    "0, 0, 0",
    "-1, 1, 0"
})
void addCases(int a, int b, int expected) {
    assertEquals(expected, calc.add(a, b));
}

@ParameterizedTest
@MethodSource("provideCases")
void fromMethod(int input, String expected) { ... }

static Stream<Arguments> provideCases() {
    return Stream.of(
        Arguments.of(1, "one"),
        Arguments.of(2, "two")
    );
}

Arguments.of(arg1, arg2, ...) 的參數順序對應 test method 的參數順序——fromMethod(int input, String expected) 對上 Arguments.of(1, "one")。JUnit 會依序解析、必要時轉換參數;無法轉成測試方法要求的型別時,測試會在參數解析階段失敗。

巢狀測試

用 @Nested 按情境分組:

class UserServiceTest {

    @Nested
    class WhenLoggedIn {
        @Test void canEditOwnProfile() { ... }
        @Test void cannotEditOthers() { ... }
    }

    @Nested
    class WhenAnonymous {
        @Test void mustLoginFirst() { ... }
    }
}

Mockito:mock 物件

測試時把外部依賴(DB、HTTP)mock 掉:

import static org.mockito.Mockito.*;

@Test
void usesRepo() {
    UserRepo repo = mock(UserRepo.class);
    when(repo.findById(1)).thenReturn(Optional.of(new User("a")));

    UserService svc = new UserService(repo);
    assertEquals("a", svc.getName(1));

    verify(repo).findById(1);
}

加依賴:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>

AssertJ:流暢斷言

寫起來比 JUnit assertion 順,失敗訊息也更清楚:

import static org.assertj.core.api.Assertions.*;

assertThat(list).hasSize(3).contains("a").doesNotContain("z");
assertThat(map).containsEntry("k", "v");
assertThat(user.getName()).startsWith("Je").endsWith("my");
assertThatThrownBy(() -> svc.find(0))
    .isInstanceOf(NotFoundException.class)
    .hasMessageContaining("not found");

在 IDE 跑 vs 在 CLI 跑

IDE(IntelliJ / VS Code)點綠色三角就能跑單一測試。CI 上用 mvn test 或 ./gradlew test,報告會產到 target/surefire-reports/ 或 build/reports/tests/。

寫測試的最低標準

  • 一個方法測一件事
  • 命名描述「行為」而不是「方法名」(addsTwoPositives 比 testAdd 好)
  • AAA 結構:Arrange、Act、Assert
  • 測試之間獨立,不共享 mutable state(用 @BeforeEach 重設)
  • 不要測 framework 的事,測自己寫的邏輯

Java 系列到此結束——從介面與抽象、例外、集合、泛型、enum / String、檔案 IO、Lambda / Stream,到 Maven / Gradle / JUnit / Mockito,寫真實 Java 專案會碰到的基礎都串完了。下一階段可以挑方向深入:Spring Boot(Web / 資料)、Android,或更貼近 JVM 的 Kotlin / Scala——哪條路都通。

參考

更新紀錄

  • 2026.11.05 內容更新
  • 2026.06.11 內容更新