第 12 章 · Maven、JUnit 5 与单元测试
本章目标:理解 Maven 项目结构与生命周期;使用 JUnit 5 编写单元测试与 @Test;掌握断言、参数化测试入门;了解 Mockito 模拟依赖;为 toolkit-demo 建立可回归的测试基线。
学时建议:4~5 小时(含 1.5 小时跟练)
前置:ch01 JDK 环境;ch05 类与包;ch11 JDBC(可选,用于集成测试示例)。
包结构说明:ch01~ch11 代码在demo.model、demo.io等子包;本章测试用demo.toolkit(与 ch01 的package demo同属 toolkit-demo,groupId为com.example.learn)。ch17 毕业项目用demo.report,请在同一 Maven 工程内按包划分。
12.1 为什么需要构建与测试
| 问题 | 工具 |
|---|---|
| 依赖管理混乱 | Maven / Gradle |
| 改代码怕回归 | 单元测试 |
| 协作目录不统一 | 标准 src/main / src/test |
| 假对象替代数据库 | Mockito |
开发 → 编写测试 → mvn test 绿灯 → 提交
↓ 红灯
修复后再测
12.2 Maven 项目结构
toolkit-demo/
├── pom.xml # 项目对象模型
├── src/
│ ├── main/
│ │ ├── java/ # 主代码
│ │ │ └── com/example/toolkit/
│ │ └── resources/ # 配置、静态资源
│ └── test/
│ ├── java/ # 测试代码
│ └── resources/
└── target/ # 编译输出(可删)
坐标(pom.xml 核心):
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>toolkit-demo</artifactId>
<version>1.0.0-SNAPSHOT</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>
</project>
与 ch01 的 toolkit-demo 保持一致使用 JDK 21;若本机只有 JDK 17,把两处21改为17即可,本章代码兼容 17。
| 元素 | 含义 |
|---|---|
groupId | 组织/公司 |
artifactId | 项目名 |
version | 版本号 |
12.3 Maven 生命周期
Maven 有三套生命周期,最常用 default:
validate → compile → test → package → verify → install → deploy
| 阶段 | 作用 |
|---|---|
compile | 编译主代码 |
test-compile | 编译测试代码 |
test | 运行单元测试 |
package | 打 jar/war |
install | 安装到本地仓库 |
clean | 清理 target(另一生命周期) |
常用命令:
cd ~/learn-java/toolkit-demo
mvn clean compile # 清理并编译
mvn test # 运行测试
mvn package -DskipTests # 打包跳过测试(慎用)
mvn dependency:tree # 查看依赖树
12.4 依赖管理
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
| scope | 说明 |
|---|---|
compile | 默认,主代码与测试均可见 |
test | 仅测试代码 |
provided | 编译需要,运行时由容器提供 |
12.5 JUnit 5 第一个测试
被测类:
package demo.toolkit;
public class DiscountCalculator {
public double discount(double price, double rate) {
if (price < 0 || rate < 0 || rate > 1) {
throw new IllegalArgumentException("非法参数");
}
return price * (1 - rate);
}
}
测试类:
package demo.toolkit;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class DiscountCalculatorTest {
@Test
void discount_applies_rate() {
DiscountCalculator calc = new DiscountCalculator();
assertEquals(80.0, calc.discount(100.0, 0.2));
}
@Test
void discount_rejects_negative_price() {
DiscountCalculator calc = new DiscountCalculator();
assertThrows(IllegalArgumentException.class,
() -> calc.discount(-1, 0.1));
}
}
运行:mvn test,Surefire 自动发现 *Test 类。
12.6 常用断言
import static org.junit.jupiter.api.Assertions.*;
assertEquals(expected, actual);
assertNotEquals(unexpected, actual);
assertTrue(condition);
assertFalse(condition);
assertNull(value);
assertNotNull(value);
assertArrayEquals(expectedArray, actualArray);
// 浮点近似
assertEquals(0.1 + 0.2, 0.3, 0.0001);
// 异常
Exception ex = assertThrows(IllegalStateException.class, () -> {
throw new IllegalStateException("msg");
});
assertEquals("msg", ex.getMessage());
| 断言 | 用途 |
|---|---|
assertEquals | 值相等 |
assertThrows | 期望抛异常 |
assertTimeout | 性能/超时 |
12.7 测试生命周期注解
import org.junit.jupiter.api.*;
class LifecycleDemo {
@BeforeAll
static void initAll() {
System.out.println("全部测试开始前一次");
}
@BeforeEach
void init() {
System.out.println("每个测试前");
}
@Test
void testOne() { }
@Test
void testTwo() { }
@AfterEach
void tearDown() {
System.out.println("每个测试后");