JUnit测试框架
面向对象程序设计(Java)
掌握测试驱动开发,提升代码质量和可靠性测试基础概念
📖 场景:计算器的噩梦
你写了一个计算器类,有个 add(int a, int b) 方法用来做加法:
public int add(int a, int b) {
return a + b;
}
😱 没有单元测试会发生什么?
- 每次修改代码后,都要手动打开计算器,一遍遍输入数字测试
- 改了其他地方的代码,不确定会不会影响这个加法功能
- 代码提交到生产环境,用户报错说"1+1=3",你一脸懵
- 想重构代码,但不敢动,怕改出bug
😊 有了单元测试会发生什么?
- 写好测试用例,一键运行,自动验证所有功能
- 改代码后,跑一遍测试,立马知道有没有破坏原有功能
- 测试代码就是活的文档,一看就知道这个方法怎么用
- 想怎么重构就怎么重构,测试不通过就改,改到通过为止
什么是单元测试?
单元测试就是用代码测试代码。在Java中,通常是针对一个类或一个方法写测试,验证它是否按预期工作。
🎯 测试的基本原则
- 独立性:测试之间不应相互依赖,每个测试应该能单独运行
- 可重复性:多次运行应得到相同结果,不能依赖外部状态
- 快速性:单元测试应该快速执行,几秒钟跑完好多个
JUnit简介
JUnit是什么?
JUnit是Java最流行的单元测试框架,自1997年诞生以来,已成为Java开发的标准工具。它提供了一套简单而强大的API来编写、运行和验证测试代码。
📊 JUnit 5的架构
JUnit 5由三个子模块组成,采用模块化设计:
| 模块 | 说明 |
|---|---|
| JUnit Platform | 测试引擎基础平台,支持多种测试引擎运行 |
| JUnit Jupiter | JUnit 5核心,包含新的编程模型和扩展模型 |
| JUnit Vintage | 兼容层,支持运行JUnit 3和JUnit 4的测试 |
🚀 版本对比
| 特性 | JUnit 4 | JUnit 5 |
|---|---|---|
| 包名 | org.junit.* | org.junit.jupiter.* |
| 生命周期注解 | @Before, @After, @BeforeClass, @AfterClass | @BeforeEach, @AfterEach, @BeforeAll, @AfterAll |
| 参数化测试 | 需要额外依赖JUnitParams | 内置支持@ParameterizedTest |
| 嵌套测试 | 不支持 | 支持@Nested |
| 动态测试 | 不支持 | 支持@TestFactory |
| 扩展模型 | 使用Runner和Rule | 更灵活的Extension API |
| 推荐程度 | 📅 旧项目兼容 | 🌟 新项目首选 |
⚙️ Maven依赖配置
<!-- JUnit 5 完整依赖(包含Platform、Jupiter、Vintage) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- 仅运行测试所需(如果使用Maven Surefire插件) -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
第一个JUnit测试 (1)
让我们从一个简单的计算器类开始,编写第一个测试。
📝 被测试的类
// src/main/java/com/example/Calculator.java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int multiply(int a, int b) {
return a * b;
}
}
这是我们要测试的简单计算器类,包含基本的算术运算方法。 接下来我们将为它编写第一个测试。
第一个JUnit测试 (2)
🧪 编写测试类
// src/test/java/com/example/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void testAdd() {
// Arrange(准备)
Calculator calculator = new Calculator();
int a = 3;
int b = 5;
int expected = 8;
// Act(执行)
int result = calculator.add(a, b);
// Assert(验证)
assertEquals(expected, result);
}
}
🎯 AAA模式说明
- Arrange:准备测试数据和对象
- Act:执行被测试的方法
- Assert:验证结果是否符合预期
在IDEA中,右键点击测试类或测试方法,选择"Run 'CalculatorTest'"即可执行测试。 绿色✅表示测试通过,红色❌表示测试失败。
常用断言方法
JUnit提供了丰富的断言方法来验证测试结果。以下是常用的断言:
🔢 数值和对象比较
@Test
void testAssertions() {
// 相等性断言
assertEquals(4, 2 + 2);
assertEquals("hello", "HELLO".toLowerCase());
// 不等性断言
assertNotEquals(5, 2 + 2);
assertNotEquals("hello", "HELLO");
// 对象相等性(重写equals()方法)
String str1 = new String("test");
String str2 = new String("test");
assertEquals(str1, str2); // 比较内容
assertNotSame(str1, str2); // 不是同一个对象
}
assertEquals 比较内容,assertNotSame 比较对象引用地址。
更多断言方法
✅ 布尔条件断言
@Test
void testBooleanAssertions() {
String text = "Hello World";
// 条件断言
assertTrue(text.contains("Hello"));
assertFalse(text.isEmpty());
// 空值断言
String nullStr = null;
String emptyStr = "";
assertNull(nullStr);
assertNotNull(emptyStr);
}
⚠️ 异常断言
@Test
void testException() {
// 测试是否抛出指定异常
assertThrows(IllegalArgumentException.class, () -> {
divideByZero();
});
// 也可以获取异常对象进行进一步验证
Exception exception = assertThrows(ArithmeticException.class, () -> {
int result = 10 / 0;
});
assertEquals("/ by zero", exception.getMessage());
}
private void divideByZero() {
throw new IllegalArgumentException("不能除以零");
}
测试生命周期
JUnit提供了多个注解来控制测试方法的执行顺序和资源管理。
🔄 生命周期注解
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void setupAll() {
System.out.println("🔧 在所有测试开始前执行一次");
// 适合初始化昂贵的资源(数据库连接等)
}
@BeforeEach
void setUp() {
System.out.println("⚡ 在每个测试方法前执行");
// 适合准备测试数据
}
@Test
void test1() {
System.out.println("📝 执行测试1");
}
@AfterEach
void tearDown() {
System.out.println("🧹 在每个测试方法后执行");
// 适合清理测试数据
}
@AfterAll
static void tearDownAll() {
System.out.println("🏁 在所有测试结束后执行一次");
// 适合释放昂贵的资源
}
}
最佳实践:使用@BeforeEach/@AfterEach保证测试独立性, 使用@BeforeAll/@AfterAll提高性能(如数据库连接)。
参数化测试 (1)
参数化测试允许我们使用不同的输入参数多次运行同一个测试方法。
🎯 @ValueSource 数据源
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class ParameterizedTestExample {
// 测试多个回文字符串
// @ValueSource 会为每个数组元素运行一次测试
// 相当于执行了4次独立的测试
@ParameterizedTest
@ValueSource(strings = {"racecar", "madam", "level", "rotor"})
void testPalindromes(String word) {
assertTrue(isPalindrome(word));
}
// 测试多个正整数
// 测试5次,分别验证1, 2, 3, 4, 5都大于0
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testPositiveNumbers(int number) {
assertTrue(number > 0);
}
// 辅助方法:判断是否为回文
private boolean isPalindrome(String word) {
return word.equals(new StringBuilder(word).reverse().toString());
}
}
@ValueSource 适合测试简单的单参数场景,
如字符串数组、数值数组等。
参数化测试 (2)
📋 @CsvSource 数据源
@ParameterizedTest
@CsvSource({
"2, 3, 5", // a=2, b=3, expected=5
"0, 0, 0", // a=0, b=0, expected=0
"-1, 1, 0", // a=-1, b=1, expected=0
"10, -5, 5" // a=10, b=-5, expected=5
})
void testAdd(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
🏭 @MethodSource 数据源
@ParameterizedTest
@MethodSource("provideTestData")
void testMultiply(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.multiply(a, b));
}
// 提供测试数据的方法
// 必须返回Stream或Arguments[]等类型
static Stream provideTestData() {
return Stream.of(
Arguments.of(2, 3, 6), // 2 * 3 = 6
Arguments.of(0, 5, 0), // 0 * 5 = 0
Arguments.of(-2, 4, -8), // -2 * 4 = -8
Arguments.of(10, 10, 100) // 10 * 10 = 100
);
}
注意:使用参数化测试需要添加依赖:junit-jupiter-params
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
@CsvSource 适合多参数测试,@MethodSource 适合复杂的测试数据生成逻辑。
测试套件和分组
当测试数量增多时,我们需要组织和分类测试。JUnit提供了标签(@Tag)和测试套件功能。
🏷️ 使用@Tag分组测试
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("slow") // 标记为慢速测试
class SlowTests {
@Test
@Tag("database") // 可以有多个标签
void testDatabaseConnection() {
// 模拟耗时的数据库操作
System.out.println("连接数据库测试...");
}
}
@Tag("fast")
@Tag("unit")
class FastTests {
@Test
void testStringOperations() {
assertEquals("HELLO", "hello".toUpperCase());
}
@Test
void testMathOperations() {
assertEquals(4, Math.addExact(2, 2));
}
}
🎯 按标签运行测试
通过Maven运行
# 只运行快速测试
mvn test -Dgroups=fast
# 排除慢速测试
mvn test -DexcludedGroups=slow
# 运行数据库相关的测试
mvn test -Dgroups=database
# 组合条件:运行快速测试但排除数据库测试
mvn test -Dgroups=fast -DexcludedGroups=database
在IDEA中运行
<!-- 在pom.xml中配置JUnit Platform -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<groups>fast</groups> <!-- 只运行标记为fast的测试 -->
<excludedGroups>slow</excludedGroups> <!-- 排除慢速测试 -->
</configuration>
</plugin>
使用标签可以有效管理大型项目的测试,在CI/CD中特别有用(如只运行快速测试)。
例如:在每次提交代码时只运行@Tag("fast")的单元测试,
在每天定时任务中运行包括@Tag("slow")在内的所有测试。
Mock测试 (1) - 为什么需要Mock
🎭 什么是Mock?
Mock(模拟)是指在测试中创建一个假的依赖对象,用来替代真实的组件。这个假对象可以模拟真实对象的行为,但完全受我们控制。
// 真实场景:UserService依赖UserRepository和EmailService
public class UserService {
private UserRepository userRepository; // 真实会访问数据库
private EmailService emailService; // 真实会发送邮件
public void registerUser(User user) {
userRepository.save(user); // 写入数据库
emailService.sendWelcome(user); // 发送邮件
}
}
// 测试时:我们不想真的写数据库、发邮件
// 所以我们创建Mock对象
UserRepository mockRepo = Mockito.mock(UserRepository.class);
EmailService mockEmail = Mockito.mock(EmailService.class);
Mock的核心特点:
- 🎯 可控制:可以预定义方法的返回值
- ⚡ 快速:没有真实IO操作,测试瞬间完成
- 🔒 隔离:不依赖外部环境,测试结果稳定
- 📊 可验证:可以检查方法是否被调用、调用了几次
🤔 为什么需要使用Mock?
| 场景 | 问题描述 | Mock解决方案 |
|---|---|---|
| 外部依赖 | 依赖数据库、网络API、文件系统 | 创建Mock对象,避免真实调用 |
| 性能问题 | 依赖组件执行缓慢(如数据库查询) | Mock对象瞬时响应,测试快速执行 |
| 不稳定因素 | 外部服务可能宕机或返回错误 | Mock行为可控,测试结果稳定 |
| 成本问题 | 调用第三方API需要付费 | Mock免费使用,降低测试成本 |
🎭 什么是Mock对象?
- 行为可控制:可以预定义方法的返回值和异常
- 调用可验证:可以验证方法是否被调用,调用次数和参数
- 轻量级:不需要真实的依赖环境
- 隔离性:确保测试只关注目标类的逻辑
Mock测试的核心思想是"隔离",让我们专注于测试目标类的逻辑,而不受外部依赖的影响。
Mock测试 (2) - Mock工具和数据生成
🛠️ Mock工具对比
| 工具 | 特点 | 优点 | 缺点 |
|---|---|---|---|
| Mockito | 最流行的Mock框架 | API简洁、文档完善、社区活跃 | 不支持静态方法Mock |
| EasyMock | 传统Mock框架 | 历史悠久、稳定可靠 | API相对繁琐、学习曲线陡峭 |
| PowerMock | 扩展Mock能力 | 支持静态方法、私有方法、构造函数 | 依赖复杂、性能较慢 |
📦 Maven依赖
<!-- Mockito - 推荐首选 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- PowerMock - 需要Mock静态方法时使用 -->
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
📚 测试数据生成工具对比
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Faker | 生成真实感数据 | 姓名、地址、邮箱等模拟数据 |
| DataFactory | 测试数据生成工厂 | 大量随机数据生成 |
📦 数据生成工具依赖
<!-- Faker - 生成真实感数据 -->
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
💡 Faker使用示例
Faker faker = new Faker();
String name = faker.name().fullName(); // "张三"
String email = faker.internet().emailAddress(); // "zhangsan@example.com"
int age = faker.number().numberBetween(18, 65); // 25-65之间的随机数
推荐优先使用 Mockito 进行Mock,配合 Faker 生成真实的测试数据。如需Mock静态方法,再考虑引入 PowerMock。
Mock测试 (3) - Mockito基础使用
� 待测试代码:OrderService
public class OrderService {
private PaymentService paymentService;
private InventoryService inventoryService;
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
public OrderResult createOrder(Order order) {
// 检查库存
inventoryService.checkStock(order.getProductId(), order.getQuantity());
// 执行扣款
PaymentResult paymentResult = paymentService.charge(order.getAmount());
if (paymentResult.isSuccess()) {
// 扣减库存
inventoryService.reduceStock(order.getProductId(), order.getQuantity());
return new OrderResult(true, "订单创建成功");
}
return new OrderResult(false, "支付失败");
}
}
🔴 不使用Mock:测试困难重重
@Test
void testCreateOrder() {
PaymentService realPayment = new RealPaymentService();
InventoryService realInventory = new RealInventoryService();
OrderService orderService = new OrderService(realPayment, realInventory);
Order order = new Order("PROD001", 2, new BigDecimal("100.00"));
OrderResult result = orderService.createOrder(order);
assertTrue(result.isSuccess());
// 问题:
// 1. 真的扣钱了!❌
// 2. 真的减少库存了!❌
// 3. 测试慢(要连数据库、支付网关)❌
// 4. 外部服务不稳定可能导致测试失败 ❌
// 5. 每次测试都有副作用,数据难清理 ❌
}
🟢 使用Mockito:测试又快又稳
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import com.github.javafaker.Faker;
import static org.junit.jupiter.api.Assertions.*;
@Test
void testCreateOrder_Success() {
Faker faker = new Faker();
// 1. 创建Mock对象:模拟依赖服务
PaymentService mockPayment = Mockito.mock(PaymentService.class);
InventoryService mockInventory = Mockito.mock(InventoryService.class);
// 2. 注入Mock对象到被测试的服务
OrderService orderService = new OrderService(mockPayment, mockInventory);
// 3. 使用Faker准备测试数据:生成真实感的订单信息
// faker.number().digits(3):生成3位随机数字(如"123"、"456"),用于构建产品ID
String productId = "PROD" + faker.number().digits(3);
// faker.number().numberBetween(1, 10):生成1到10之间的随机整数,表示购买数量
int quantity = faker.number().numberBetween(1, 10);
// faker.commerce().price():生成商业价格(如"123.45"、"1,234.56")
BigDecimal amount = new BigDecimal(faker.commerce().price().replace(",", ""));
Order order = new Order(productId, quantity, amount);
// 4. 配置Mock行为:当调用这些方法时,返回预设值
Mockito.when(mockInventory.checkStock(productId, quantity)).thenReturn(true);
Mockito.when(mockPayment.charge(amount))
.thenReturn(new PaymentResult(true, "支付成功"));
// 5. 执行被测试方法
OrderResult result = orderService.createOrder(order);
// 6. 验证结果:断言返回值符合预期
assertTrue(result.isSuccess());
assertEquals("订单创建成功", result.getMessage());
// 7. 验证交互:确认Mock对象被正确调用
Mockito.verify(mockInventory).checkStock(productId, quantity);
Mockito.verify(mockInventory).reduceStock(productId, quantity);
Mockito.verify(mockPayment).charge(amount);
}
Mockito的核心价值:隔离依赖、控制行为、验证调用。
测试只关注OrderService的业务逻辑,不关心支付和库存的具体实现。
Mock测试 (4) - 高级Mock技巧
🔍 常用Mock方法详解
1️⃣ mock() - 创建Mock对象
List mockList = Mockito.mock(List.class);
说明:创建一个假的对象,所有方法调用都返回默认值(null、0、false等)。
使用场景:当你不需要真实对象,只想控制其行为时使用。
2️⃣ when().thenReturn() - 定义返回值
Mockito.when(mockList.get(0)).thenReturn("Hello");
Mockito.when(mockList.size()).thenReturn(1);
说明:告诉Mock对象,当调用指定方法时返回什么值。
使用场景:模拟外部依赖的返回结果,测试不同的业务路径。
注意:参数必须精确匹配,"Hello" 和 "hello" 是不同的调用。
3️⃣ when().thenThrow() - 定义异常
Mockito.when(mockList.get(1)).thenThrow(new IndexOutOfBoundsException());
说明:让Mock对象在调用时抛出异常。
使用场景:测试异常处理逻辑,确保代码能正确处理错误情况。
4️⃣ 链式thenReturn() - 多次调用返回不同值
Mockito.when(mockList.get(0)).thenReturn("First").thenReturn("Second");
// 第一次调用返回"First",第二次及以后返回"Second"
说明:连续调用时返回不同的值。
使用场景:测试需要多次调用同一方法的场景(如重试机制)。
5️⃣ verify() - 验证方法调用
Mockito.verify(mockList).get(0); // 验证get(0)被调用1次
Mockito.verify(mockList, Mockito.times(2)).add(Mockito.any()); // 验证add()被调用2次
Mockito.verify(mockList, Mockito.never()).clear(); // 验证clear()从未被调用
说明:检查Mock对象的方法是否被调用,以及调用次数。
使用场景:验证业务逻辑是否按预期执行(如:订单创建时是否真的调用了支付服务)。
验证模式:times(n)、never()、atLeastOnce()、atMost(n)等。
💡 高级Mock技巧
1️⃣ spy() - 部分Mock,保留真实方法
List spyList = Mockito.spy(new ArrayList<>());
spyList.add("real"); // 调用真实方法
Mockito.when(spyList.size()).thenReturn(100); // Mock size方法
说明:创建一个"间谍"对象,默认调用真实方法,但可以Mock特定方法。
使用场景:大部分逻辑用真实实现,只Mock少量依赖的方法。
注意:不要Mock被spy对象的其他方法调用,否则会调用真实方法而非Mock版本。
2️⃣ ArgumentCaptor - 参数捕获
ArgumentCaptor captor = ArgumentCaptor.forClass(String.class);
Mockito.verify(emailService).sendEmail(captor.capture(), Mockito.anyString());
String capturedEmail = captor.getValue(); // 获取捕获的参数
说明:捕获方法调用时的参数值,用于验证传入的参数是否符合预期。
使用场景:当你需要验证传递给方法的参数内容(如:发送的邮件地址、插入数据库的数据)。
注意:只能在verify()中使用,不能在when()中使用。
3️⃣ thenAnswer() - 自定义返回逻辑
Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(invocation -> {
Integer index = invocation.getArgument(0);
return "Item " + index;
});
说明:根据调用参数动态计算返回值,比固定的thenReturn更灵活。
使用场景:返回值依赖于输入参数,需要基于参数计算返回结果时。
注意:可以访问invocation对象获取所有参数和方法信息。
4️⃣ doNothing() / doThrow() - Void方法Mock
Mockito.doNothing().when(mockList).clear(); // Void方法不执行任何操作
Mockito.doThrow(new RuntimeException()).when(mockList).clear(); // Void方法抛异常
说明:Mock没有返回值的方法(void方法)的行为。
使用场景:当调用void方法时不希望执行真实逻辑,或者让void方法抛出异常。
注意:必须在when()之前使用doXxx(),语法是`doXxx().when(mock).method()`而不是`when(mock.method()).doXxx()`。
Mock最佳实践:优先使用Mockito,配合Faker生成测试数据, 使用Builder模式创建复杂对象,让Mock测试既真实又高效!
Maven与JUnit集成 (1)
在实际开发中,我们通常使用Maven来管理项目依赖和构建过程。理解Maven如何与JUnit协作是重要的技能。
🔗 Maven与JUnit的关系
Maven构建生命周期:
validate → compile → test → package → verify → install → deploy
↑
JUnit在这里执行
📁 Maven标准项目结构
my-project/
├── pom.xml # Maven配置文件
├── src/
│ ├── main/ # 源代码
│ │ ├── java/
│ │ └── resources/
│ └── test/ # 测试代码 (JUnit在这里运行)
│ ├── java/
│ └── resources/
└── target/ # 编译输出目录
├── classes/ # 源代码编译结果
└── test-classes/ # 测试代码编译结果
Maven会自动在 mvn test 阶段扫描并运行所有JUnit测试。
Maven与JUnit集成 (2)
🚀 在IDEA中运行Maven测试
IDEA Maven工具窗口操作:
方法1:使用Maven工具栏
1. 打开右侧Maven工具窗口
2. 展开 Lifecycle
3. 双击 "test" 命令
4. Maven会自动运行所有JUnit测试
方法2:使用快捷键
- Ctrl + Shift + F10 (运行所有测试)
- Ctrl + Shift + T (创建测试类)
⚙️ Maven surefire插件配置
<!-- pom.xml中的配置 -->
<build>
<plugins>
<!-- Maven Surefire Plugin - 负责运行单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
核心要点:Maven的mvn test命令会自动扫描并运行所有JUnit测试,
理解这个集成机制对于现代Java开发至关重要!
代码校验体系:不只靠“写测试”
真实项目里,“正确性”不是靠某一种手段兜底,而是靠多层防线叠加:
| 层次 | 手段 | 解决的问题 |
|---|---|---|
| 编译期 | 类型系统、编译器报错、IDE 检查 | 低成本提前发现问题(类型不匹配、空指针风险、未处理异常等) |
| 运行期 | 参数校验、状态校验、断言(assert) | 把“坏输入/坏状态”挡在边界处,减少隐蔽 Bug |
| 测试期 | JUnit 单元测试/集成测试 | 把需求变成可重复验证的规则,支持重构 |
| 线上期 | 日志、监控、告警 | 快速定位异常路径,降低 MTTR(修复平均时间) |
一句话:断言/契约让“问题更早爆炸”,JUnit 让“爆炸可重复”,日志让“爆炸可定位”。
运行期校验 ①:assert(断言)
断言用于检查程序员认为“不可能发生”的内部不变式,默认关闭,适合开发/测试环境。
public final class Range {
private final int start;
private final int end;
public Range(int start, int end) {
assert start <= end : "start must be <= end";
this.start = start;
this.end = end;
}
public int size() {
int size = end - start;
assert size >= 0;
return size;
}
}
如何启用断言?
# 启用所有断言
java -ea com.example.Main
# 禁用所有断言(默认就是禁用)
java -da com.example.Main
- 不要用断言做用户输入校验:生产环境可能没开
-ea - 用户/接口边界校验:用
IllegalArgumentException/NullPointerException(由 API 约定)更可靠
提示:你可以用 SomeClass.class.desiredAssertionStatus() 检查某个类的断言是否启用。
运行期校验 ②:契约式设计(Design by Contract)
把一个方法当成“合同”,通常包含三类条款:
- 前置条件(Precondition):调用者必须满足
- 后置条件(Postcondition):方法返回后必须成立
- 不变式(Invariant):对象整个生命周期都必须成立
import java.util.Objects;
public final class BankAccount {
private long balance;
public void deposit(Long amount) {
Objects.requireNonNull(amount, "amount");
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
long old = balance;
balance += amount;
if (balance < old) {
throw new IllegalStateException("overflow");
}
}
}
经验法则:断言更像“开发者自检”,异常更像“对外契约”。对外接口优先用异常表达契约。
测试 + 日志:让可验证与可定位同时成立
TDD(红-绿-重构)
- 红:先写一个失败的测试,明确“我想要什么”
- 绿:写最少代码让测试通过
- 重构:在测试保护下优化结构
@Test
void parse_invalidInput_throwsException() {
assertThrows(IllegalArgumentException.class,
() -> Parser.parse("x=??"));
}
日志级别(让问题可定位)
- ERROR:必须修的失败路径
- WARN:异常但可继续
- INFO:关键业务里程碑
- DEBUG/TRACE:排查细节(默认不打开)
import java.util.logging.Level;
import java.util.logging.Logger;
public class App {
private static final Logger LOG = Logger.getLogger(App.class.getName());
void pay(long amount) {
LOG.log(Level.INFO, "pay start amount={0}", amount);
}
}
分工:测试用于“验证行为”,日志用于“定位路径”。不要用日志代替测试,也不要用测试去验证日志内容。
测试最佳实践
编写高质量、可维护的测试需要遵循一些重要的原则和模式。
📝 测试命名规范
// ❌ 不好的命名
@Test
void test1() { }
@Test
void testCalculator() { }
// ✅ 好的命名(遵循:methodName_condition_expectedResult)
@Test
void add_twoPositiveNumbers_returnsCorrectSum() { }
@Test
void divide_byZero_throwsArithmeticException() { }
@Test
void withdraw_insufficientBalance_throwsException() { }
// 或者使用 given_when_then 格式
@Test
void givenValidAmount_whenDeposit_thenBalanceIncreases() { }
🏗️ AAA模式(Arrange-Act-Assert)
@Test
void transferMoney_sufficientBalance_updatesBothAccounts() {
// Arrange(准备)- 设置测试数据和初始状态
Account fromAccount = new Account(1000);
Account toAccount = new Account(500);
Bank bank = new Bank();
bank.addAccount(fromAccount);
bank.addAccount(toAccount);
// Act(执行)- 调用被测试的方法
bank.transferMoney(fromAccount, toAccount, 300);
// Assert(验证)- 检查结果是否符合预期
assertEquals(700, fromAccount.getBalance());
assertEquals(800, toAccount.getBalance());
}
好的测试不仅是质量的保障,更是代码的活文档。 遵循这些实践原则,让测试成为开发的有力工具!