Java 系列收尾。語法、OOP、集合、stream 都寫完之後,要讓專案真正跑得起來、發得出去、有人能維護,靠的就是這一層:建置工具與測試。純 javac + java 寫小程式還行,正式專案得有工具管依賴、編譯、打包、測試。Java 主流兩套:Maven 與 Gradle。
Maven 還是 Gradle?
| Maven | Gradle | |
|---|---|---|
| 設定檔 | 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 |
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("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/。
寫測試的最低標準
- 一個方法測一件事
- 命名描述「行為」而不是「方法名」(
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——哪條路都通。
Latest Updates
- 2026.06.11 Content updated
