在单元测试中,when() 和 given() 是 Mockito 框架中用于模拟依赖行为的核心方法,它们的作用完全相同,区别仅在于语法风格与可读性偏好。
一、when() 与 given() 的作用
| 方法 | 作用 |
|---|---|
when(mock.method()).thenReturn(value) | 告诉 Mockito:当调用 mock 对象的某个方法时,返回指定的值。用于设置模拟行为。 |
given(mock.method()).willReturn(value) | 与 when() 完全等价,是 Mockito 为支持 BDD(行为驱动开发)风格 提供的别名。 |
✅ 本质相同:两者都用于“桩定”(Stub)一个模拟对象的方法返回值或行为,不会执行真实逻辑。
二、when() 与 given() 的区别
| 维度 | when() | given() |
|---|---|---|
| 来源 | Mockito 原生语法(传统) | Mockito 为 BDD 风格引入的别名 |
| 语义风格 | 命令式(Imperative):“当调用时,返回…” | 声明式(Declarative):“给定这个调用,应该返回…” |
| 可读性 | 通用,广泛使用 | 更贴近自然语言,适合 BDD 团队 |
| 推荐场景 | 传统 Java 开发团队 | 偏好 Gherkin 语言(如 Cucumber)或强调可读性的团队 |
| 是否推荐 | ✅ 推荐(主流、稳定) | ✅ 推荐(语义更清晰,但需团队统一) |
📌 关键结论:
given()是when()的语义增强版,两者功能完全一致。
在实际项目中,推荐统一使用一种风格,避免混用。
三、常见写法对比
| 场景 | when() 写法 | given() 写法 |
|---|---|---|
| 返回固定值 | when(mock.getService()).thenReturn("OK") | given(mock.getService()).willReturn("OK") |
| 抛出异常 | when(mock.process()).thenThrow(new Exception()) | given(mock.process()).willThrow(new Exception()) |
| 多次调用不同返回值 | when(mock.getValue()).thenReturn("A").thenReturn("B") | given(mock.getValue()).willReturn("A").willReturn("B") |
| 匹配任意参数 | when(mock.validate(anyString())).thenReturn(true) | given(mock.validate(anyString())).willReturn(true) |
| 匹配特定参数 | when(mock.calculate(100)).thenReturn(200) | given(mock.calculate(100)).willReturn(200) |
💡
willReturn()是thenReturn()的 BDD 风格别名,willThrow()是thenThrow()的别名。
四、推荐使用哪一个?
| 推荐指数 | 说明 |
|---|---|
✅ 强烈推荐 given() | 在新项目或注重可读性、团队协作的场景中,given() 更接近自然语言,语义清晰、意图明确,尤其适合保险、金融等高合规要求的系统。 |
✅ 也可使用 when() | 在传统 Java 团队或遗留项目中,when() 更普遍,无需强制迁移。 |
📌 最终建议:
在你的团队中统一选择一种风格,并写入《编码规范》。
推荐新项目使用given(),因为它更符合“给定前提,验证结果”的测试思维模型,与 BDD 流程天然契合。
五、综合性单元测试示例(保险核保系统)——使用 given() 风格
背景:在人身保险系统中,根据客户年龄、健康状态、理赔金额判断是否自动核保通过。
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDate;
import java.time.Period;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
// 使用 MockitoExtension 自动注入 @Mock 和 @InjectMocks
@ExtendWith(MockitoExtension.class)
class AutoUnderwritingServiceTest {
// 模拟外部依赖:健康评估服务
@Mock
private HealthAssessmentService healthAssessmentService;
// 模拟外部依赖:理赔金额配置服务
@Mock
private ClaimLimitConfigService claimLimitConfigService;
// 被测试的系统:自动核保服务
@InjectMocks
private AutoUnderwritingService underwritingService;
// 每个测试前初始化(可选,因使用 @ExtendWith)
@BeforeEach
void setUp() {
// Mockito 会自动处理,此处仅作说明
}
/**
* 测试场景:客户年龄 30 岁、健康状态良好、理赔金额 40000 元 → 应自动通过核保
* 使用 BDD 风格:Given... When... Then...
* 重点:使用 given() 设置模拟行为,语义清晰
*/
@Test
void shouldApproveClaimWhenCustomerIsYoungHealthyAndAmountWithinLimit() {
// Given:给定前提条件(模拟依赖行为)
Long customerId = 1001L;
LocalDate birthDate = LocalDate.now().minusYears(30); // 年龄30岁
Double claimAmount = 40000.0; // 申请金额
String expectedHealthStatus = "GOOD"; // 健康状态
Double maxAllowedLimit = 50000.0; // 系统允许的最大金额
// 使用 given() 模拟健康服务返回“良好”
given(healthAssessmentService.getHealthStatus(customerId))
.willReturn(expectedHealthStatus);
// 使用 given() 模拟配置服务返回该年龄段的最高限额
given(claimLimitConfigService.getMaxClaimLimitForAgeGroup(30))
.willReturn(maxAllowedLimit);
// When:执行被测试的方法
UnderwritingResult result = underwritingService.evaluateClaim(customerId, birthDate, claimAmount);
// Then:验证结果是否符合预期
assertThat(result.isApproved()).isTrue(); // 核保应通过
assertThat(result.getReason()).isEqualTo("客户年龄30岁,健康良好,金额在限额内");
// 验证依赖被正确调用(行为验证)
verify(healthAssessmentService, times(1)).getHealthStatus(customerId);
verify(claimLimitConfigService, times(1)).getMaxClaimLimitForAgeGroup(30);
}
/**
* 测试场景:客户年龄 65 岁 → 应拒绝自动核保
* 使用 given() 模拟健康状态(虽不决定结果,但必须存在)
*/
@Test
void shouldRejectClaimWhenCustomerAgeExceeds60() {
// Given:给定客户年龄超过60岁
Long customerId = 1002L;
LocalDate birthDate = LocalDate.now().minusYears(65); // 65岁
String healthStatus = "GOOD"; // 健康状态不影响结果
Double claimAmount = 10000.0;
// 使用 given() 模拟健康服务调用(必须,否则报错)
given(healthAssessmentService.getHealthStatus(customerId))
.willReturn(healthStatus);
// When:执行核保逻辑
UnderwritingResult result = underwritingService.evaluateClaim(customerId, birthDate, claimAmount);
// Then:验证结果
assertThat(result.isApproved()).isFalse(); // 应拒绝
assertThat(result.getReason()).isEqualTo("客户年龄超过60岁,需人工核保");
}
/**
* 测试场景:健康评估服务抛出异常 → 系统应优雅降级,拒绝核保
* 使用 given() + willThrow() 模拟异常分支
*/
@Test
void shouldFailGracefullyWhenHealthServiceThrowsException() {
// Given:给定健康评估服务调用时抛出异常
Long customerId = 1003L;
LocalDate birthDate = LocalDate.now().minusYears(40);
Double claimAmount = 25000.0;
// 使用 willThrow() 模拟服务异常(BDD 风格)
given(healthAssessmentService.getHealthStatus(customerId))
.willThrow(new RuntimeException("健康系统连接失败"));
// When:执行核保逻辑
UnderwritingResult result = underwritingService.evaluateClaim(customerId, birthDate, claimAmount);
// Then:验证系统未崩溃,返回拒绝结果
assertThat(result.isApproved()).isFalse();
assertThat(result.getReason()).isEqualTo("健康评估服务异常,请人工介入");
}
/**
* 测试场景:理赔金额超过限额 → 应拒绝
* 使用 given() 模拟动态配置值
*/
@Test
void shouldRejectClaimWhenClaimAmountExceedsMaximumLimit() {
// Given:给定客户年龄35岁,但申请金额超出其限额
Long customerId = 1004L;
LocalDate birthDate = LocalDate.now().minusYears(35);
Double claimAmount = 60000.0; // 超出限额
Double maxLimit = 50000.0; // 实际允许限额
// 使用 given() 模拟配置服务返回限额
given(healthAssessmentService.getHealthStatus(customerId))
.willReturn("GOOD");
given(claimLimitConfigService.getMaxClaimLimitForAgeGroup(35))
.willReturn(maxLimit);
// When
UnderwritingResult result = underwritingService.evaluateClaim(customerId, birthDate, claimAmount);
// Then
assertThat(result.isApproved()).isFalse();
assertThat(result.getReason()).isEqualTo("理赔金额超出系统允许上限 50000.0");
}
/**
* 测试场景:使用 any() 匹配任意参数,测试通用逻辑
* 使用 given() 模拟模糊匹配
*/
@Test
void shouldReturnDefaultReasonWhenAnyHealthStatusIsProvided() {
// Given:给定任意客户ID,健康状态为任意字符串,都返回“需人工核保”
Long customerId = 1005L;
LocalDate birthDate = LocalDate.now().minusYears(50); // 50岁
Double claimAmount = 30000.0;
// 使用 any() 匹配任意字符串参数(不关心具体值)
given(healthAssessmentService.getHealthStatus(anyLong()))
.willReturn("UNKNOWN");
// 使用 anyInt() 匹配任意整数(年龄计算由服务内部处理)
given(claimLimitConfigService.getMaxClaimLimitForAgeGroup(anyInt()))
.willReturn(40000.0);
// When
UnderwritingResult result = underwritingService.evaluateClaim(customerId, birthDate, claimAmount);
// Then
assertThat(result.isApproved()).isFalse();
assertThat(result.getReason()).isEqualTo("健康状况未知,需人工核保");
}
}
六、为什么推荐使用 given()?——语义对比
| 传统写法 | BDD 写法 | 优势 |
|---|---|---|
when(mock.get()).thenReturn("OK") | given(mock.get()).willReturn("OK") | given 更清晰表达“前提条件” |
when(service.process()).thenThrow(ex) | given(service.process()).willThrow(ex) | willThrow 更贴近“预期行为” |
when(repository.findById(1)).thenReturn(user) | given(repository.findById(1)).willReturn(user) | 读起来像“给定 ID=1,返回用户”,像自然语言 |
✅ 在保险、金融等高合规系统中,代码的可读性就是可审计性。
given()更容易让非技术人员(如测试、风控、合规)理解测试意图。
七、最佳实践总结
| 建议 | 说明 |
|---|---|
✅ 统一使用 given() | 新项目优先采用,提升团队协作效率 |
✅ 避免混用 when() 和 given() | 同一项目中风格统一,减少认知负担 |
✅ willReturn() / willThrow() 是首选 | BDD 风格完整,语义更完整 |
✅ 每个测试都遵循 Given-When-Then 结构 | 逻辑清晰,便于维护 |
✅ 配合 AssertJ 使用 assertThat() | 断言更流畅,如 assertThat(result).isApproved().and().hasReason("...") |
✅ 用 any()、eq()、argThat() 精确匹配参数 | 避免过度模拟导致测试脆弱 |
八、补充:如果团队坚持用 when()?
完全没问题!只要团队一致,when() 是行业标准。
但如果你正在引入新成员、或希望提升代码的“业务可读性”,given() 是更优选择。
🔚 最终建议:
在你的 Spring Boot 保险系统中,使用given()+willReturn()+assertThat()组合,
让你的单元测试不仅能跑通,更能被所有人读懂——这才是高质量代码的终极目标。
Mockito中when与given的区别

被折叠的 条评论
为什么被折叠?



