JUnit怎么写?一篇让你彻底掌握单元测试的实战指南
目录导读
- JUnit是什么?为什么必须学?
- JUnit环境搭建:5分钟搞定第一个测试
- JUnit核心注解全解析
- 断言方法:如何验证测试结果?
- 参数化测试:一次编写,多组数据验证
- Mockito配合JUnit:解决依赖难题
- 常见错误与最佳实践
- 问答精选:解决你90%的疑惑
JUnit是什么?为什么必须学?
JUnit是Java生态中最流行的单元测试框架,它的核心思想是:用代码测试代码。
- 测试驱动开发(TDD)基石:先写测试,再写实现
- 回归测试利器:修改代码后一键运行,立刻知道是否破坏原有功能
- CI/CD必备:Jenkins、GitLab CI等工具自动执行测试
关键问答
问:JUnit 4和JUnit 5哪个更推荐?
答:推荐JUnit 5,它模块化更强,支持Java 8+特性,且完全向后兼容JUnit 4。
问:单元测试只测公共方法吗?
答:通常只测公共API,但若私有方法逻辑复杂,可通过反射或重构方式来测试。
JUnit环境搭建:5分钟搞定第一个测试
第一步:添加Maven依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
Gradle用户:
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
第二步:编写第一个测试类
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
void testAdd() {
Calculator calc = new Calculator();
int result = calc.add(2, 3);
assertEquals(5, result, "2+3应该等于5");
}
}
第三步:运行测试
- IDE直接运行:IntelliJ或Eclipse中点击绿色三角
- Maven运行:
mvn test - Gradle运行:
gradle test
输出示例:
JUnit核心注解全解析
| 注解 | 作用 | 执行时机 |
|---|---|---|
@Test |
标记一个测试方法 | 每次测试运行时 |
@BeforeEach |
每个测试方法前执行 | 准备数据/对象 |
@AfterEach |
每个测试方法后执行 | 清理资源 |
@BeforeAll |
所有测试前执行一次 | 静态方法,初始化全局资源 |
@AfterAll |
所有测试后执行一次 | 静态方法,释放全局资源 |
@Disabled |
禁用某测试方法 | 不执行,显示为跳过 |
完整示例
class DatabaseTest {
private static Connection conn;
@BeforeAll
static void setupConnection() {
conn = DriverManager.getConnection("jdbc:h2:mem:test");
}
@BeforeEach
void cleanTable() {
conn.createStatement().execute("DELETE FROM users");
}
@Test
void testInsert() {
// 测试插入逻辑
}
@AfterEach
void logTestEnd() {
System.out.println("测试结束");
}
@AfterAll
static void closeConnection() throws Exception {
conn.close();
}
}
断言方法:如何验证测试结果?
断言是测试的灵魂,JUnit 5提供丰富的断言方法:
常用断言
assertEquals(4, calculator.multiply(2, 2)); // 相等
assertNotEquals(5, calculator.multiply(2, 2)); // 不相等
assertTrue(result > 0); // 布尔真
assertFalse(result < 0); // 布尔假
assertNull(obj); // 对象为空
assertNotNull(obj); // 对象非空
assertThrows(IllegalArgumentException.class, () -> {
calculator.divide(1, 0);
}); // 预期异常
assertTimeout(Duration.ofMillis(100), () -> {
Thread.sleep(50);
}); // 超时断言
进阶:assertAll组合断言
assertAll("用户属性验证",
() -> assertEquals("小明", user.getName()),
() -> assertTrue(user.getAge() > 0),
() -> assertNotNull(user.getEmail())
);
// 即使某个断言失败,其他断言仍会执行,并收集所有错误
参数化测试:一次编写,多组数据验证
当需要测试多组输入输出时,参数化测试避免重复代码。
添加依赖(JUnit 5内置)
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.*;
常见数据来源
// 方式1:@ValueSource(简单值)
@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "level"})
void testPalindrome(String word) {
assertTrue(isPalindrome(word));
}
// 方式2:@CsvSource(表格数据)
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
// 方式3:@MethodSource(自定义数据工厂)
@ParameterizedTest
@MethodSource("provideUsers")
void testUserValidation(User user, boolean expected) {
assertEquals(expected, validator.isValid(user));
}
static Stream<Arguments> provideUsers() {
return Stream.of(
Arguments.of(new User("", 20), false),
Arguments.of(new User("张三", 17), false),
Arguments.of(new User("李四", 18), true)
);
}
优点:大幅减少重复代码,测试覆盖率更高。
Mockito配合JUnit:解决依赖难题
真实项目中,类常依赖数据库、外部API等,Mockito可以模拟这些依赖。
添加Mockito依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
实战示例
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) // JUnit 5集成
class OrderServiceTest {
@Mock
private PaymentGateway paymentGateway; // 模拟支付网关
@InjectMocks
private OrderService orderService; // 自动注入mock
@Test
void testCreateOrder() {
// 设置mock行为
when(paymentGateway.charge(anyDouble())).thenReturn(true);
Order order = orderService.createOrder(100.0);
// 验证支付网关被调用
verify(paymentGateway).charge(100.0);
assertEquals(OrderStatus.PAID, order.getStatus());
}
@Test
void testPaymentFailed() {
when(paymentGateway.charge(anyDouble())).thenReturn(false);
assertThrows(PaymentException.class, () -> {
orderService.createOrder(200.0);
});
}
}
重要技巧:
@Mock创建模拟对象@InjectMocks自动注入mock到被测类when().thenReturn()定义mock行为verify()验证方法是否被调用
常见错误与最佳实践
错误1:测试依赖于执行顺序
// 错误示例
@Test void test1() { /* 修改全局状态 */ }
@Test void test2() { /* 依赖test1的状态 */ }
正确做法:每个测试独立,用@BeforeEach初始化。
错误2:测试数据库依赖
// 避免
@Test
void testQuery() {
// 直接连接生产数据库
}
正确做法:使用H2内存数据库或Mock DAO层。
错误3:断言信息写得太少
// 不推荐 assertEquals(5, result); // 推荐 assertEquals(5, result, "计算2+3时预期为5,实际得到:" + result);
最佳实践清单
- 命名规范:
testMethodName_Scenario_ExpectedResult,如testDivide_ByZero_ThrowsException - 一个测试只测一个概念:不要在一个@test中测试多个无关场景
- 使用TDD:先写失败测试,再写实现
- 保持测试快速:避免网络IO、数据库连接
- 代码覆盖率不是目标:重点测试核心逻辑和边界情况
问答精选:解决你90%的疑惑
问:如何测试私有方法?
答:不推荐直接测试私有方法,应测试调用私有方法的公有方法,若必须测,可用PowerMock或提取为工具类(改为公有)。
问:测试中如何模拟时间?(如过期判断)
答:使用java.time.Clock注入,测试时传入固定时间的Clock实例。
问:JUnit 5中如何忽略测试?
答:使用@Disabled("原因说明")。
问:多个测试类怎么组织?
答:每个类对应一个被测类,按包结构组织,例如src/test/java/com/example/service/UserServiceTest.java。
问:如何生成测试报告?
答:Maven使用maven-surefire-report-plugin,Gradle使用test-report任务,CI工具(如Jenkins)会自动收集测试报告。
JUnit的核心在于:
- 注解驱动:通过
@Test、@BeforeEach等管理生命周期 - 断言验证:
assertEquals、assertThrows等确保结果正确 - 参数化:用
@ParameterizedTest减少重复代码 - Mock协作:Mockito隔离外部依赖
- 独立性:每个测试互不干扰,可重复执行
当你掌握了这些技术,编写JUnit测试不再是一种负担,而是提升代码质量和开发效率的利器,从今天开始,为你的每一个Java方法都写上一个测试吧!
最后送你一句话:没有测试的代码,不是完整的代码。