CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件测试入门/课时/Mockito 与 Mock
RawBlame
课时目录12 节

软件测试入门

  1. 01测试金字塔
  2. 02JUnit 5 基础
  3. 03Mockito 与 Mock
  4. 04Spring Boot 测试
  5. 05MockMvc 接口测试
  6. 06集成测试
  7. 07TDD 与可维护性
  8. 08前端单元测试
  9. 09E2E 与 Playwright
  10. 10覆盖率与质量门禁
  11. 11CI 中的测试
  12. 12综合练习

Mockito 与 Mock

mock、when/then、verify。

35 分钟

Mockito 与 Mock

这一课你将能做到

  1. 用 @ExtendWith(MockitoExtension.class) + @Mock / @InjectMocks 给 CodeForge 的 SubmissionService、AuthService 写出毫秒级单元测试
  2. 掌握 stub(when/thenReturn/thenThrow)、verify 行为断言与 ArgumentCaptor 参数捕获三件套
  3. 用 mockStatic 隔离 sa-token 的 StpUtil 静态调用,测通注册成功全链路

为什么要学

单元测试要隔离被测类:数据库、Redis、HTTP、时钟都不该拖慢或污染测试。人话:Mock 是「听话的替身演员」——你规定它怎么答,再看被测 Service 有没有按剧本调度。

CodeForge 的 Service 层依赖 MyBatis-Flex 的 Mapper(SubmissionMapper、UserMapper)。用 Mockito 测「语言白名单、代码长度校验、用户名重复拒绝」这类编排逻辑,完全不必起 MySQL。真实的 SubmissionServiceTest 就是这么写的——本课带你从读懂到写出同款,并补上它还没覆盖的进阶手法(参数捕获、静态 mock)。

本课练的是不启 Spring 的纯单元写法;容器里的替身(@MockitoBean)是下一课的事。这个分界线本身就是面试考点。

先建立图像

真实运行:  Controller → Service → Mapper → MySQL
单元测试:  测试代码 → Service → @Mock Mapper(假的)
                              ↑
                     when(...).thenReturn(...)   规定台词
                     verify(...).方法(...)       查出勤
术语人话常见 API
Mock假对象@Mock / mock(X.class)
Stub规定「问什么答什么」when(...).thenReturn(...)
Verify检查「有没有按预期调用」verify(mock).方法(...)
InjectMocks把 mock 注入被测类@InjectMocks Service
MockedStatic给静态方法换替身mockStatic(StpUtil.class)

不要过度 mock:纯函数不用 mock;测 Service 才 mock Mapper / 外部 Client;测 SQL 正确性 Mock 救不了,那是集成测试的事(第 6 课)。

跟着做

步骤 1:搭最小骨架,测判题提交的 happy path

import com.codeforge.entity.Submission;
import com.codeforge.mapper.SubmissionMapper;
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 static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class SubmissionMockitoTest {

    @Mock
    SubmissionMapper submissionMapper;

    @InjectMocks
    SubmissionService service;

    @Test
    void submit_构造记录并置pending() {
        when(submissionMapper.insert(any(Submission.class))).thenReturn(1);

        Submission result = service.submit(3L, 5L, "java", "class A {}");

        assertThat(result.getStatus()).isEqualTo("pending");
        assertThat(result.getUserId()).isEqualTo(3L);
        assertThat(result.getLanguage()).isEqualTo("java");
        verify(submissionMapper).insert(any(Submission.class));
    }
}

应看到:测试毫秒级通过,全程无数据库。若 @InjectMocks 注入失败,检查 SubmissionService 的构造器参数(Lombok @RequiredArgsConstructor 生成的那个)类型是否与 @Mock 字段一一对应。

步骤 2:补校验失败分支(不需要 stub 的用例)

submit 的校验发生在 Mapper 调用之前,所以这类用例不用写任何 when:

@Test
void submit_非法语言直接拒绝() {
    assertThatThrownBy(() -> service.submit(3L, 5L, "brainfuck", "code"))
        .isInstanceOf(BusinessException.class)
        .hasMessage("不支持的语言类型");
}

@Test
void submit_空代码拒绝() {
    assertThatThrownBy(() -> service.submit(3L, 5L, "java", " "))
        .isInstanceOf(BusinessException.class)
        .hasMessageContaining("代码长度");
}

@Test
void submit_非法课时拒绝且不落库() {
    assertThatThrownBy(() -> service.submit(3L, 0L, "java", "code"))
        .isInstanceOf(BusinessException.class);

    verify(submissionMapper, never()).insert(any());
}

第三条把 never() 与异常断言组合——「拒绝的同时没写库」才是完整规格。

步骤 3:ArgumentCaptor——比 verify(any()) 更强的证据

verify(insert(any())) 只证明「调了」,ArgumentCaptor 能证明「调的时候塞进去的是什么」:

import org.mockito.ArgumentCaptor;

@Test
void submit_落库的是待判题记录() {
    when(submissionMapper.insert(any(Submission.class))).thenReturn(1);

    service.submit(3L, 5L, "java", "class A {}");

    ArgumentCaptor<Submission> captor = ArgumentCaptor.forClass(Submission.class);
    verify(submissionMapper).insert(captor.capture());

    Submission saved = captor.getValue();
    assertThat(saved.getStatus()).isEqualTo("pending");
    assertThat(saved.getCreatedAt()).isNotNull();
    assertThat(saved.getCode()).isEqualTo("class A {}");
}

人话:stub 决定 Service 拿到什么,captor 决定你能检查它还回去什么。两者配合才闭环。

步骤 4:测注册逻辑的「用户名已存在」分支

@ExtendWith(MockitoExtension.class)
class AuthServiceDuplicateTest {

    @Mock UserMapper userMapper;
    @InjectMocks AuthService authService;

    @Test
    void register_用户名已存在时拒绝且不插入() {
        when(userMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(1L);

        assertThatThrownBy(() ->
            authService.register("existing", "a@example.com", "123456"))
            .isInstanceOf(BusinessException.class)
            .hasMessage("用户名已存在");

        verify(userMapper, never()).insert(any(User.class));
    }
}

注意真实 AuthService 的查重靠 selectCountByQuery 返回值大于 0 判断——stub 时返回 1L 就能让分支亮起来。读被测实现再写 stub,是单测的日常。

步骤 5:mockStatic 隔离静态调用,测注册成功全链路

注册成功路径会调 StpUtil.login(...) 和 StpUtil.getTokenValue()——没有 Web 上下文会炸。Mockito 5 默认内置 inline mockmaker,静态方法可以直接换替身:

import cn.dev33.satoken.stp.StpUtil;
import org.mockito.MockedStatic;

@Test
void register_成功时加密入库并返回token() {
    when(userMapper.selectCountByQuery(any(QueryWrapper.class))).thenReturn(0L);
    when(userMapper.insert(any(User.class))).thenReturn(1);

    try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
        stp.when(StpUtil::getTokenValue).thenReturn("token-abc");

        String token = authService.register("newbie", "n@example.com", "123456");

        assertThat(token).isEqualTo("token-abc");
        stp.verify(() -> StpUtil.login(anyLong()));
    }

    ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
    verify(userMapper).insert(captor.capture());
    // 密码必须是哈希,不能是明文
    assertThat(captor.getValue().getPasswordHash())
        .isNotEqualTo("123456")
        .startsWith("$2");
}

try-with-resources 保证静态替身用完即释放,不泄漏到其他用例——这一行写漏会引发整套件连环红。

步骤 6:匹配器规则与定向运行

// 一旦某个参数用了匹配器,同一调用的其它参数也要用匹配器
when(userMapper.selectOneByQuery(any())).thenReturn(user);      // 全匹配器
when(mapper.count(eq("java"))).thenReturn(1L);                  // eq 包裸值

混用裸值与匹配器能编译、运行时才报错,是初学者最常见的 Mockito 崩溃点。

mvn test -Dtest=SubmissionMockitoTest,AuthServiceDuplicateTest

讲清楚要点

  1. 单测要快且稳,所以用 mock 挡掉一切 I/O:真库留给集成测试,毫秒级红绿才养得起「改码即跑」的习惯。
  2. stub 只规定返回值,行为还要靠 assert / verify 证明:「调用了 mock」≠「业务对」,两者缺一就是假绿。
  3. 只 mock 直接依赖:mock 隔壁的邻居的邻居,测出来的是「和自己对话」,没有回归价值。
  4. 关键入参优先 eq 精确值:any() 到处飞会掩盖「传错参数」类 bug,宽松匹配按需开放。
  5. 构造器注入让 @InjectMocks 更可靠:CodeForge 的 Service 用 Lombok @RequiredArgsConstructor,字段一目了然;字段注入 + 大量 setter 会让替身装配变得玄学。
  6. 静态调用用 mockStatic 且必须 try-with-resources:Mockito 5 起开箱即用,但替身作用域要手动圈死。

你可能会懵的地方

  • @Mock 和 @MockitoBean 是不是一回事? 概念像,作用域不同。@Mock 只活在测试类里,配 @InjectMocks 手工装配;@MockitoBean(Boot 3.4+,旧版叫 @MockBean)替换的是 Spring 容器里的 Bean。下一课展开。

  • 为什么 verify 失败提示「Wanted but not invoked」? 被测代码没走到那条分支:可能是 stub 条件没对上(参数不匹配导致返回默认值),也可能校验提前抛了异常。先用 IDE 断点确认流程走向,再改 stub。

  • UnnecessaryStubbingException 是错误吗? 是严格模式的保护:你 stub 了没被用到的方法,多半意味着「用例走不到你以为的分支」。删掉多余 stub,或修正测试意图,不要急着换 lenient 模式。

  • 要不要 verifyNoMoreInteractions? 它能抓多余调用,但对重构极其敏感。入门只对关键副作用(发信、扣款、删数据、写库)做 verify / never,全量「查户口」留给真正需要零信任的模块。

  • 什么时候不该用 Mockito? 纯函数(直接 new + 断言)、SQL 正确性(mock 答的是你编的,只有真库能证明)、以及被测类本身就是薄代理时——单测只会复读实现。

错法对照

错法问题正解
单测打真 MySQL/Redis慢、脆、难并行mock Mapper 与 RedisTemplate
只 when 无 assert假绿断言返回值 + 关键 verify
mock 一切连被测类自己测试无意义只 mock 直接依赖
any() 到处飞掩盖错误入参关键参数 eq,必要时 captor
用 mock 验证 SQL 写对没有假象Testcontainers 集成测试
mockStatic 不关流泄漏污染其他用例try-with-resources 包住

卡点急救

  1. when(...) 要求参数必须全是匹配器或全是裸值:报 InvalidUseOfMatchersException 时,把裸值换成 eq(x),一个不留。
  2. @InjectMocks 字段为 null:确认被测类依赖走构造器(Lombok @RequiredArgsConstructor),且 @Mock 类型与构造器参数类型完全一致(泛型也算)。
  3. 静态调用测试抛 sa-token 上下文异常:说明 StpUtil 没被 mockStatic 拦到——检查 try 块是否覆盖了整个调用段、import 的 StpUtil 是否与生产代码同包同名。

面试常问

  • 什么时候该 mock,什么时候不该? 该 mock:跨进程 I/O(数据库、Redis、HTTP、消息)、不稳定因素(时钟、随机)。不该 mock:被测类内的纯逻辑、值对象、以及你要验证的 SQL 本身。判断标准——mock 之后结论还可信吗?可信就 mock,不可信就上集成测试。

  • stub 和 verify 的区别?只做一个会漏什么? stub 是「输入侧」:规定依赖怎么响应;verify 是「输出侧」:检查被测类对依赖的调用。只 stub 不 verify,会漏「多调/漏调/调错依赖」类 bug(如误发邮件);只 verify 不 assert,会漏返回值加工逻辑的错误。

  • 静态方法怎么测? 首选重构:把静态调用包成可注入的组件(如 Clock、限流器接口)。退而求其次用 Mockito 5 的 mockStatic,配 try-with-resources 圈定作用域。面试时先说重构优先,再给 mockStatic 兜底,是加分答法。

自测三问

  1. stub 与 verify 各解决什么问题?只做其中一个会漏什么?
  2. 什么情况下不该使用 Mockito?
  3. ArgumentCaptor 比 verify(insert(any())) 强在哪?

小结一句话

Mock 替身、Stub 返回值、Verify 行为、Captor 抓参数、mockStatic 圈静态——Service 层单元测试的五件套。

延伸

  • Mockito 官方文档
  • JUnit 5 · 扩展模型
  • 下一课:test-spring 进入 Spring 容器,用 @WebMvcTest 切片与 @MockitoBean 测 Bean 装配与 Web 层。
加载本节练习…
← 上一课JUnit 5 基础下一课 →Spring Boot 测试