Mockito测试案例

wen java案例 1

深入Mockito测试案例:从零到实战的单元测试最佳实践(附详细代码解析)

Mockito测试案例

目录导读

  1. 为什么Mockito是Java单元测试的“标配”?
  2. Mockito核心概念快速扫盲(Mock/Spy/InjectMocks)
  3. 实战案例一:基础Mock与验证(模拟UserService)
  4. 实战案例二:复杂场景——异常、回调与Answer接口
  5. 实战案例三:静态方法、构造器与Final类(使用Inline MockMaker)
  6. Mockito与JUnit5、Spring Boot的集成技巧
  7. 高频问题FAQ:为什么我的Mock不生效?
  8. 总结与避坑指南:5条铁律让测试更健壮

为什么Mockito是Java单元测试的“标配”?

在真实项目中,一个Service层方法往往依赖DAO、外部API、消息队列等,如果直接new真实对象,测试速度慢、不稳定(比如网络抖动),而且无法模拟极端异常,Mockito通过创建假对象(Mock),让你能精确控制依赖的行为,从而只测试当前类的逻辑,据统计,全球约70%的Java项目使用Mockito,因为它语法简洁、与JUnit无缝集成,且支持“行为驱动开发(BDD)”风格。

核心价值:Mockito把“依赖隔离”做到了极致,让单元测试真正“单元化”。

Mockito核心概念快速扫盲

  • @Mock:创建mock对象,该对象的所有方法返回默认值(如null、0、false)。
  • @Spy:保留真实对象的方法,但可以局部打桩(stub)覆盖个别方法。
  • @InjectMocks:将mock或spy对象自动注入到被测类中(基于构造器、setter或字段注入)。
  • when(...).thenReturn(...):打桩,定义“何时调用返回什么”。
  • verify(...):验证某个方法是否被调用,以及调用次数、顺序。

小提示:使用@ExtendWith(MockitoExtension.class)(JUnit5)或@RunWith(MockitoJUnitRunner.class)(JUnit4)来启用Mockito注解。

实战案例一:基础Mock与验证(模拟UserService)

假设有一个UserService,它依赖UserDao来查询用户。

public class UserService {
    private final UserDao userDao;
    public UserService(UserDao userDao) { this.userDao = userDao; }
    public String getUsername(Integer id) {
        User user = userDao.findById(id);
        return (user == null) ? "unknown" : user.getName();
    }
}

测试代码

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock UserDao userDao;
    @InjectMocks UserService userService;
    @Test
    void testGetUsername_whenUserExists() {
        User mockUser = new User(1, "Alice");
        when(userDao.findById(1)).thenReturn(mockUser);
        String name = userService.getUsername(1);
        assertEquals("Alice", name);
        verify(userDao).findById(1); // 验证确实调用了
    }
    @Test
    void testGetUsername_whenUserNull() {
        when(userDao.findById(2)).thenReturn(null);
        assertEquals("unknown", userService.getUsername(2));
    }
}

问答环节

verifywhen有什么区别?
when是打桩,调用mock方法时返回预设结果verify事后检查,确认某个方法是否被调用过,以及调用次数(times(2)never()等)。

实战案例二:复杂场景——异常、回调与Answer接口

如果依赖方法需要抛出异常,或需要根据参数动态计算返回值,怎么办?

// 模拟异常
doThrow(new RuntimeException("DB down")).when(userDao).delete(anyInt());
// 使用Answer动态返回
when(userDao.findById(anyInt())).thenAnswer(invocation -> {
    Integer id = invocation.getArgument(0);
    return id > 100 ? new User(id, "BigID") : null;
});

回调场景:有些老代码使用回调接口,Mockito借助doAnswer可以模拟异步回调。

doAnswer(invocation -> {
    Callback cb = invocation.getArgument(0);
    cb.onSuccess("fakeResult");
    return null;
}).when(mockClient).call(any(Callback.class));

问答环节

when(...).thenReturn(...)doReturn(...).when(...)有什么区别?
:在方法有返回值时两者等价,但doReturn不执行真实方法,适合间谍对象(@Spy)无返回值void方法(此时只能用doThrow/doAnswer),建议:对void方法一律用doXxx;对非void方法用when更直观。

实战案例三:静态方法、构造器与Final类(使用Inline MockMaker)

Mockito默认无法mock静态方法和final类,但通过配置mockito-inline依赖,可以开启实验性支持。

依赖配置(Maven)

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>

测试静态方法(例如UUID.randomUUID()):

try (MockedStatic<UUID> mocked = mockStatic(UUID.class)) {
    mocked.when(UUID::randomUUID).thenReturn(new UUID(0, 0));
    // 调用被测方法,验证逻辑
}

构造器mock:使用try (MockedConstruction<File> mocked = mockConstruction(File.class))

警告:静态mock会污染其他测试,务必在try-with-resources块内使用,确保自动关闭。

问答环节

:mock静态方法后,测试方法内部创建了new对象,怎么mock构造器?
:使用mockConstruction,在MockedConstruction.Context里获取构造参数并设置默认行为,但注意:过度使用静态mock往往暗示设计缺陷,优先考虑重构代码(如把静态方法封装进可注入类)。

Mockito与JUnit5、Spring Boot的集成技巧

在Spring Boot测试中,有两种常见用法:

  • 纯单元测试:不用Spring上下文,直接@Mock + @InjectMocks,跑得飞快。
  • 整合测试(@WebMvcTest / @ServiceTest):配合@MockBean(Spring包装的Mockito)。
@WebMvcTest(UserController.class)
class UserControllerTest {
    @Autowired MockMvc mockMvc;
    @MockBean UserService userService;
    @Test
    void testGetUser() throws Exception {
        when(userService.getUsername(1)).thenReturn("Bob");
        mockMvc.perform(get("/user/1"))
               .andExpect(status().isOk())
               .andExpect(content().string("Bob"));
    }
}

关键点@MockBean会把UserService的mock对象放进Spring容器,替换掉真实Bean,但重置状态在每个测试后自动完成。

高频问题FAQ:为什么我的Mock不生效?

  • 情况1:Mock对象被when打桩了,但实际调用的是真实对象。
    原因@InjectMocks注入失败(构造器参数匹配不上),导致被测类持有的是真实依赖。解决:检查注入方式,选择最明确的构造器注入并加上@Autowired

  • 情况2verify报错“Wanted but not invoked”。
    原因:参数不匹配(例如anyInt() vs eq(1)混用)。解决:统一使用eq()或纯anyInt(),禁止混合使用(除any()外)。

  • 情况3:调用when时报错“Unnecessary stubbing”。
    原因:测试中打桩了但没有调用对应方法。解决:删除多余的stub,或者使用lenient()(Mockito 2.20+):lenient().when(...).thenReturn(...)

  • 情况4:mock静态方法报错“Mockito cannot mock this class”。
    检查:是否引入mockito-inline,以及当前Mockito版本是否兼容。

总结与避坑指南:5条铁律让测试更健壮

  1. 铁律一只mock外部依赖,Mockito用于隔离,不要对被测类自身的方法打桩,否则测试意义降低。
  2. 铁律二精确控制参数,优先使用eq()anyInt()contains()等匹配器,避免使用any()匹配所有导致误判。
  3. 铁律三verify只验证必要的交互,过度验证会脆弱,别把每个方法都verify一遍。
  4. 铁律四测试命名与行为对齐:建议使用method_should_expectedBehavior_whenCondition格式,如getUsername_shouldReturnUnknown_whenUserNotFound
  5. 铁律五隔离静态mock:使用try-with-resources,并考虑把静态方法抽到类里以提升可测性。

最终提醒:Mockito让测试代码更简单,但真正的高质量测试还需要结合“断言时机”、“覆盖率分析”和“代码重构”,建议持续重构代码以降低对过度mock的依赖——这会使测试不仅快,而且更能反映业务逻辑的正确性。

抱歉,评论功能暂时关闭!