在单元测试中, Mockito 框架中 when() 和 given() 两者的作用和区别

Mockito中when与given的区别

在单元测试中,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() 组合
让你的单元测试不仅能跑通,更能被所有人读懂——这才是高质量代码的终极目标。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值