一、JUnit 5 核心基础(入门必备)
1. 核心依赖(Maven/Gradle)
Maven(pom.xml)
xml
<dependencies>
<!-- JUnit 5 核心依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- 可选:参数化测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>
Gradle(build.gradle)
groovy
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.2'
}
test {
useJUnitPlatform()
}
2. 基础代码示例
步骤 1:待测试的业务类(比如企业常见的订单金额计算)
java
运行
package com.example.demo.service;
/**
* 企业常见场景:订单金额计算服务
*/
public class OrderAmountService {
// 计算订单最终金额(原价 - 折扣 + 运费)
public double calculateFinalAmount(double originalAmount, double discount, double freight) {
// 边界校验:金额不能为负
if (originalAmount < 0 || discount < 0 || freight < 0) {
throw new IllegalArgumentException("金额不能为负数");
}
// 折扣不能超过原价
if (discount > originalAmount) {
throw new IllegalArgumentException("折扣不能超过原价");
}
return originalAmount - discount + freight;
}
// 判断订单是否为大额订单(金额≥1000)
public boolean isLargeOrder(double finalAmount) {
return finalAmount >= 1000.0;
}
}
步骤 2:JUnit 5 测试类
java
运行
package com.example.demo.service;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
/**
* OrderAmountService的单元测试类
* 企业规范:测试类命名=待测试类+Test,测试方法命名=test+功能+场景
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 整个测试类共用一个实例
public class OrderAmountServiceTest {
private OrderAmountService orderAmountService;
// 每个测试方法执行前初始化(@BeforeEach:每个方法执行前;@BeforeAll:类初始化时执行一次)
@BeforeEach
void setUp() {
orderAmountService = new OrderAmountService();
}
// 1. 基础场景测试:正常金额计算
@Test
@DisplayName("测试正常订单金额计算") // 自定义测试名称,便于CI/CD报告阅读
void testCalculateFinalAmount_NormalCase() {
// given:测试前置条件
double original = 500.0;
double discount = 50.0;
double freight = 10.0;
// when:执行待测试方法
double result = orderAmountService.calculateFinalAmount(original, discount, freight);
// then:断言结果符合预期
assertEquals(460.0, result, 0.001); // 0.001是浮点精度容差
}
// 2. 异常场景测试:折扣超过原价
@Test
@DisplayName("测试折扣超过原价时抛出异常")
void testCalculateFinalAmount_DiscountExceedOriginal() {
// given
double original = 500.0;
double discount = 600.0;
double freight = 10.0;
// when & then:断言抛出指定异常,且异常信息匹配
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> orderAmountService.calculateFinalAmount(original, discount, freight)
);
assertEquals("折扣不能超过原价", exception.getMessage());
}
// 3. 参数化测试:批量测试不同场景(企业高频用法,减少重复代码)
@ParameterizedTest
@CsvSource({
"1000.0, true", // 大额订单
"999.9, false", // 非大额订单
"0.0, false", // 零金额
"1500.5, true" // 带小数的大额订单
})
@DisplayName("参数化测试-判断是否为大额订单")
void testIsLargeOrder(double amount, boolean expected) {
boolean result = orderAmountService.isLargeOrder(amount);
assertEquals(expected, result);
}
// 4. 禁用测试(临时跳过,企业用于未完成的功能)
@Test
@Disabled("待修复:浮点精度问题")
void testCalculateFinalAmount_WithPrecision() {
// 待实现的测试逻辑
}
// 测试完成后清理资源(企业用于关闭连接、释放内存等)
@AfterEach
void tearDown() {
orderAmountService = null;
}
}
3. 核心注解说明(企业面试必问)
| 注解 | 作用 |
|---|---|
@Test | 标记测试方法 |
@DisplayName | 自定义测试名称,提升可读性(CI/CD 报告中更友好) |
@BeforeEach | 每个测试方法执行前执行(初始化对象、准备数据) |
@BeforeAll | 测试类初始化时执行一次(静态方法,初始化全局资源如数据库连接) |
@AfterEach | 每个测试方法执行后执行(清理资源) |
@AfterAll | 测试类所有方法执行完后执行一次(静态方法,关闭连接) |
@Disabled | 禁用测试方法 |
@ParameterizedTest | 参数化测试入口 |
@CsvSource | 参数化测试的数据源(CSV 格式,企业常用) |
@TestInstance | 控制测试实例的生命周期(PER_CLASS:一个实例;PER_METHOD:默认,每个方法一个实例) |
二、企业实际应用场景
1. 核心业务逻辑验证
- 场景:电商系统的订单计算、支付金额校验、库存扣减逻辑。
- 作用:确保核心算法(如优惠计算、税费计算)在各种边界条件下正确,避免生产环境资损。
- 企业实践:每个核心方法都要覆盖「正常场景 + 异常场景 + 边界场景」。
2. 工具类 / 通用方法测试
- 场景:日期格式化、字符串处理、加密解密工具类。
- 作用:保证通用方法的复用性和正确性,避免 “一处错处处错”。
- 企业实践:结合参数化测试,批量覆盖不同输入(如空字符串、特殊字符、极值)。
3. 异常处理逻辑验证
- 场景:接口调用超时、参数非法、数据库连接失败。
- 作用:确保异常被正确捕获和处理,避免系统崩溃。
- 企业实践:用
assertThrows断言异常类型和信息,覆盖所有异常分支。
4. 单元测试与 CI/CD 集成(企业核心流程)
-
场景:Jenkins/GitLab CI 自动构建时执行单元测试。
-
作用:代码提交后自动验证,提前发现问题,避免缺陷流入测试环境。
-
企业实践:
- 配置 CI 脚本,要求单元测试通过率 100% 才能合并代码;
- 生成测试覆盖率报告(如 JaCoCo),要求核心代码覆盖率≥80%。
5. 测试替身(Mock)结合使用(企业高频)
- 场景:待测试方法依赖数据库、第三方接口等外部服务。
- 作用:用 Mock(如 Mockito)模拟外部依赖,避免测试受外部环境影响。
- 示例(结合 Mockito):
java
运行
// 依赖注入的Service
@Service
public class OrderService {
@Autowired
private UserService userService; // 依赖的外部服务
public boolean isVipOrder(Long orderId) {
Long userId = getUserIdByOrderId(orderId);
return userService.isVip(userId); // 调用外部服务
}
}
// 测试类(Mock UserService)
@Test
void testIsVipOrder() {
// 模拟UserService的返回值
UserService mockUserService = Mockito.mock(UserService.class);
Mockito.when(mockUserService.isVip(1L)).thenReturn(true);
// 注入Mock对象
OrderService orderService = new OrderService();
ReflectionTestUtils.setField(orderService, "userService", mockUserService);
// 测试
boolean result = orderService.isVipOrder(1001L);
assertTrue(result);
}
三、企业面试高频问题
1. 基础类问题
Q1:JUnit 4 和 JUnit 5 的区别?
- 核心包:JUnit4 是
org.junit,JUnit5 拆分为junit-jupiter-api(API)、junit-jupiter-engine(运行时); - 注解:JUnit4 的
@Before→JUnit5 的@BeforeEach,@After→@AfterEach,@Test(expected = Exception.class)→JUnit5 的assertThrows; - 扩展性:JUnit5 支持 Lambda 表达式、参数化测试更灵活,支持 Java 8 + 特性;
- 架构:JUnit5 是模块化设计,支持多种运行时(如 Vintage 引擎兼容 JUnit4)。
Q2:单元测试的原则(FIRST)?
- Fast:测试要快(毫秒级),避免依赖外部服务;
- Independent:测试之间相互独立,不依赖执行顺序;
- Repeatable:可重复执行,无论环境如何结果一致;
- Self-validating:自动断言结果,无需人工检查;
- Timely:测试与代码同步编写(TDD:测试驱动开发)。
2. 实践类问题
Q3:如何保证单元测试的有效性?
- 覆盖核心场景:正常流程、边界值(如金额 0、最大值)、异常流程;
- 避免 “假测试”:不要只写
assertTrue(true),要针对业务逻辑断言; - 测试粒度:单个测试方法只测一个功能点,命名清晰(如
testXxx_场景_预期结果); - 隔离外部依赖:用 Mock 模拟数据库、接口等,避免测试受外部环境影响。
Q4:单元测试覆盖率越高越好吗?
- 不是。覆盖率是参考指标,不是目标;
- 核心代码(如订单计算、支付逻辑)必须高覆盖率(≥80%);
- 非核心代码(如简单的 getter/setter)无需强制覆盖;
- 避免为了覆盖率写 “无效测试”,重点是覆盖 “逻辑分支” 而非 “代码行数”。
Q5:如何测试私有方法?
- 不建议直接测试私有方法(私有方法是内部实现,应通过公有方法间接测试);
- 特殊场景:用反射(
ReflectionTestUtils)调用私有方法,或把私有方法改为包级访问(不推荐); - 企业实践:如果私有方法逻辑复杂,建议拆分为独立的公有工具类,再测试工具类。
3. 进阶类问题
Q6:单元测试和集成测试的区别?
| 维度 | 单元测试 | 集成测试 |
|---|---|---|
| 测试粒度 | 单个方法 / 类 | 多个类 / 模块 / 服务 |
| 依赖处理 | Mock 外部依赖 | 真实依赖(如数据库、接口) |
| 执行速度 | 快(毫秒级) | 慢(秒 / 分钟级) |
| 目的 | 验证逻辑正确性 | 验证模块间交互正确性 |
Q7:TDD(测试驱动开发)的流程?
- 编写一个失败的单元测试(覆盖要实现的功能);
- 编写最少的代码让测试通过;
- 重构代码(优化结构,不改变功能);
- 重复上述步骤,逐步完善功能;
- 企业价值:提前暴露设计问题,代码更易测试,减少后期重构成本。
四、企业最佳实践总结
- 测试命名规范:测试类 = 业务类 + Test,测试方法 = test + 功能 + 场景(如
testCalculateFinalAmount_DiscountExceedOriginal); - 覆盖核心场景:正常流程、边界值、异常流程、空值 / 非法参数;
- 避免测试耦合:不依赖外部服务,用 Mock 隔离;
- 参数化测试:批量覆盖多场景,减少重复代码;
- CI 集成:强制单元测试通过才能合并代码,生成覆盖率报告;
- 不测试无关逻辑:如 getter/setter、简单的日志打印(除非有特殊逻辑);
- 测试可维护:避免硬编码,用常量 / 配置管理测试数据,便于后期修改。