Java Maven Gradle JUnit test

[Java] 建置工具與測試

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

Maven 還是 Gradle?

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

新專案的話,Spring Boot 系多用 Maven、Android / Kotlin 系多用 Gradle。會其中一個,另一個的 README 看得懂就行。

Maven 標準目錄

my-app/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/App.java
    │   └── resources/
    │       └── application.properties
    └── 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.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <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>
</project>

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

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
providedservlet-api(容器提供)
runtimeJDBC driver
testJUnit

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("org.junit.jupiter:junit-jupiter:5.10.0")
}

tasks.test {
    useJUnitPlatform()
}

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

常用指令:

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

./gradlew 是 Gradle Wrapper,跟著倉庫一起 commit,所有人就會用同一版 Gradle,連事先安裝都不用。

JUnit 5 基礎

JUnit 5(Jupiter)是現代標準。不少舊專案還在用 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() { ... }
}

生命週期 annotation

何時
@BeforeAll整個 class 跑前一次(必須 static
@BeforeEach每個 @Test
@AfterEach每個 @Test
@AfterAll整個 class 跑完一次(必須 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)
);

參數化測試

@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 在 runtime 用反射按順序塞進去,型別不符會丟 ParameterResolutionException

巢狀測試

@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/

寫測試的最低標準

  • 一個方法測一件事
  • 命名描述「行為」而不是「方法名」(addsTwoPositivestestAdd 好)
  • 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——哪條路都通。

Latest Updates

  • 2026.06.11 Content updated