单元测试 + Mockito 开发指南

分为上篇:基础入门(纯案例,零基础上手)、下篇:结合现有项目实操(对应当前模板服务测试代码)


上篇 基础入门:JUnit5 + Mockito 零基础教程

一、核心原理

  1. 什么是单元测试?

只测单个 Java 方法/类,不启动整个网站、不连接数据库、不调用第三方接口,只验证代码逻辑写得对不对。

类比:单独测试洗衣机的「脱水功能」,不用接水管、不用通电整台机器运行。

  1. 什么是 Mockito?

模拟工具,专门用来造「假对象」。

项目里 Service 会依赖数据库、OSS、其他接口,真实环境启动麻烦;

用 Mockito 可以伪造依赖,告诉它「你查询就返回这个数据」「上传文件不用真传」,做到只测自己写的业务代码。

  1. 组合关系
  • JUnit5:单元测试运行框架,提供 @Test、断言(判断结果对错)
  • Mockito:模拟依赖框架,提供 @Mock、when、verify
  • 两者搭配:Java 后端标准单元测试方案。

二、环境准备(Maven 项目)

你的项目 pom.xml 已经自带依赖,无需额外添加:

复制代码
<!-- 核心测试包:包含 JUnit5 + Mockito -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <!-- 禁用旧版 JUnit4,只使用 JUnit5 -->
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
  </exclusions>
</dependency>

三、第一步:编写待测试业务代码

  1. 模拟依赖(仓库接口,代表数据库)

路径:src/main/java/com/demo/repo/UserRepo.java

复制代码
package com.demo.repo;

// 模拟数据库查询接口
public interface UserRepo {
    // 根据ID查用户名
    String getUserName(Long userId);
    // 根据ID删除用户
    boolean deleteUser(Long userId);
}
  1. 编写业务 Service(真正要测试的代码)

路径:src/main/java/com/demo/service/UserService.java

复制代码
package com.demo.service;

import com.demo.repo.UserRepo;

public class UserService {
    // 依赖数据库接口
    private final UserRepo userRepo;

    // 构造函数注入依赖
    public UserService(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    // 业务1:获取用户名
    public String getName(Long id) {
        return userRepo.getUserName(id);
    }

    // 业务2:删除用户并返回提示
    public String removeUser(Long id) {
        boolean isSuccess = userRepo.deleteUser(id);
        return isSuccess ? "删除成功" : "删除失败";
    }
}

四、第二步:编写单元测试代码

测试代码统一放在:src/test/java/ 目录下

路径:src/test/java/com/demo/service/UserServiceTest.java

完整测试代码

复制代码
package com.demo.service;

import com.demo.repo.UserRepo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

// 固定写法:开启 Mockito 支持(JUnit5 必须加)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    // 1. @Mock:伪造一个 假的 UserRepo 对象(不连真实数据库)
    @Mock
    private UserRepo userRepo;

    // 被测试的业务类
    private UserService userService;

    // 单个测试方法
    @Test
    void testUserServiceLogic() {
        // ========== 步骤1:初始化被测试对象,传入假依赖 ==========
        userService = new UserService(userRepo);

        // ========== 步骤2:伪造依赖的返回值 when(对象.方法).thenReturn(结果) ==========
        // 规则:调用 getUserName(1L),固定返回 "张三"
        when(userRepo.getUserName(1L)).thenReturn("张三");
        // 规则:调用 getUserName(2L),固定返回 "李四"
        when(userRepo.getUserName(2L)).thenReturn("李四");

        // deleteUser(1L) 第一次调用返回true,第二次返回false
        when(userRepo.deleteUser(1L))
                .thenReturn(true)
                .thenReturn(false);

        // ========== 步骤3:执行业务代码 + 断言(判断结果是否正确) ==========
        String name1 = userService.getName(1L);
        assertEquals("张三", name1); // 预期结果=张三,正确则测试通过

        String name2 = userService.getName(2L);
        assertEquals("李四", name2);

        assertEquals("删除成功", userService.removeUser(1L));
        assertEquals("删除失败", userService.removeUser(1L));

        // ========== 步骤4:校验依赖是否被正常调用 verify ==========
        // 校验:deleteUser(1L) 一共调用了 2 次
        verify(userRepo, times(2)).deleteUser(1L);
        // 校验:getUserName(999L) 从未被调用
        verify(userRepo, never()).getUserName(999L);
    }
}

五、第三步:运行测试 & 常用命令

方式1:IDE 直接运行(IDEA/Eclipse)

右键测试类 → Run UserServiceTest

  • 绿色对勾:测试通过
  • 红色报错:代码逻辑/测试规则写错

六、核心语法速记

  1. @Mock:创建假对象,模拟数据库、OSS、接口等外部依赖
  2. when(对象.方法(参数)).thenReturn(返回值):给假对象设定「固定返回数据」
  3. assertEquals(预期值, 实际值):断言,判断代码运行结果是否正确
  4. verify(对象, times(n)).方法():校验方法调用次数
  5. verify(对象, never()).方法():校验方法从未调用

下篇 进阶实操:结合现有模板项目(TemplateDesignerService)

一、项目背景回顾

  1. 被测目标

TemplateDesignerService 模板设计器核心服务,三大核心功能:

  1. 替换 JSON 中图片路径

  2. 根据模板类型生成 rules.json/flow.json/manifest.json 运行配置

  3. 草稿发布:打包ZIP、上传OSS、保存版本记录

  4. 项目特点

  • 依赖多:数据库仓库、OSS存储、导出服务、路由服务等
  • 全部使用 @Mock 伪造依赖,不连真实数据库、不上传真实文件
  • 测试覆盖:正常流程、边界场景(空预览图、文件丢失、非法类型)

二、整体原理

  1. 用 @Mock 伪造所有外部依赖(Repository、ObjectStorageService 等)
  2. @BeforeEach:每个测试执行前,统一初始化被测试 Service、注册模板策略
  3. when():伪造数据库查询、OSS下载/上传的返回数据
  4. 调用 Service 业务方法,执行逻辑
  5. assertEquals/assertThrows:判断生成文件、返回结果是否正确
  6. verify:判断 OSS 上传、数据库保存等操作是否正常执行

三、代码结构拆解

  1. 类头部注解 & 依赖伪造

    // JUnit5 + Mockito 固定开启注解

    @ExtendWith(MockitoExtension.class)

    class TemplateDesignerServiceTest {

    复制代码
     // 简单测试:JSON 图片路径替换
     @Test
     void rewriteDesignerAssetPathsInJson_replacesDraftIdInUrl() {
         String in = "{\"styleImage\":\"/storage/designer/assets/5/foo.png\"}";
         // 调用工具方法:把草稿ID 5 改成 99
         String out = TemplateDesignerService.rewriteDesignerAssetPathsInJson(in, 5L, 99L);
         // 断言:路径成功替换
         assertTrue(out.contains("/storage/designer/assets/99/"));
         assertFalse(out.contains("/storage/designer/assets/5/"));
     }
    
     // ========== 所有外部依赖:全部 @Mock 伪造(不连真实库/存储) ==========
     @Mock
     private TemplateDraftRepository draftRepository;
     @Mock
     private TemplateBuildRepository buildRepository;
     @Mock
     private ObjectStorageService objectStorageService; // 假OSS
     // ... 其他仓库、服务

理解:这里所有带 @Mock 的对象,都是「假的」,不会操作真实资源。

  1. 前置初始化 @BeforeEach

每个 @Test 方法执行前都会运行,统一初始化环境:

复制代码
@BeforeEach
void setUp() {
    // 1. 注册模板类型策略(证件照、风格人像、新版设计器)
    TemplateTypeRegistry registry = new TemplateTypeRegistry();
    registry.register("ID_PHOTO_V1", new IdPhotoTemplateTypeStrategy());
    registry.register("STYLE_PORTRAIT_V1", new StylePortraitTemplateTypeStrategy());

    // 2. 伪造路由规则:新版设计器 复用 风格人像逻辑
    TemplateTypeRoutingService routing = routingMockMatchingDictionary();

    // 3. 创建导出服务、被测试主服务
    exportService = new TemplateDesignerExportService(...);
    designerService = new TemplateDesignerService(...);

    // 4. 反射赋值:设置文件存储路径
    ReflectionTestUtils.setField(designerService, "storageBasePath", "./storage/designer-test");
}

作用:统一准备测试环境,避免每个测试方法重复写初始化代码。

  1. 工具方法(复用测试数据)

项目底部几个私有方法,是测试专用工具,简化代码:

  1. buildDraft():快速创建假的「模板草稿」实体类
  2. buildValidTemplateZip():快速生成合法ZIP压缩包字节数据
  3. readZipEntryBytes():读取ZIP包里的文件,用来校验内容

作用:不用每次手动构造复杂对象、文件,提高测试代码复用性。

  1. 第一类测试:生成运行配置文件(5个用例)

以证件照模板为例:

复制代码
@Test
void testGenerateRuntimeFilesForDraft_IdPhotoV1() throws Exception {
    // 1. 构造假草稿数据
    TemplateDraft draft = buildDraft("ID_PHOTO_V1", "{\"formState\":...}");
    // 2. 创建临时文件夹
    Path tempDir = Files.createTempDirectory("designer_runtime_id_");
    try {
        // 3. 反射调用 Service 私有方法(测试内部逻辑)
        ReflectionTestUtils.invokeMethod(designerService, "generateRuntimeFilesForDraft", draft, tempDir, "1.0.0");
        // 4. 读取生成的 rules.json,断言内容是否符合证件照规则
        JsonNode rules = objectMapper.readTree(Files.readString(tempDir.resolve("rules.json")));
        assertEquals("C_CUTOUT_COMPOSE", rules.get("pipelineMode").asText());
    } finally {
        // 5. 收尾:删除临时文件
        Files.deleteIfExists(tempDir.resolve("rules.json"));
    }
}

场景覆盖:

  • 证件照、风格人像、新版设计器:验证规则复用
  • 非法模板类型:assertThrows 断言抛出异常(拦截非法数据)
  1. 第二类测试:草稿发布(4个核心用例)

以「正常发布+OSS上传」为例,核心流程:

复制代码
@Test
void testPublishDraft_UploadsToOssAndWritesPackageObjectKey() throws Exception {
    // 1. 构造假草稿、旧版本数据
    TemplateDraft draft = buildDraft(...);
    TemplateVersion baseVersion = new TemplateVersion();

    // 2. 伪造依赖返回值:when + thenReturn
    when(draftRepository.findById(10L)).thenReturn(Optional.of(draft));
    when(objectStorageService.getObject(...)).thenReturn(baseZip); // 假OSS返回旧ZIP包
    when(exportServiceMock.exportZip(...)).thenReturn(outZip);    // 假打包服务返回新ZIP

    // 3. 执行业务:发布草稿
    TemplateDesignerService.PublishResponse resp = publishService.publishDraft(10L, req);

    // 4. verify 校验:OSS上传、数据库保存 必须被执行
    verify(objectStorageService).putObject(...);
    verify(buildRepository).save(any(TemplateBuild.class));

    // 5. 断言:返回结果、ID、URL 是否正确
    assertEquals(101L, resp.getBuildId());
}

场景覆盖(边界测试):

  1. 正常发布:上传ZIP、保存版本、更新封面
  2. 预览图为空:不修改原有封面(verify(..., never()) 校验不执行保存)
  3. 本地预览图存在:替换ZIP内的预览图片
  4. 预览图文件丢失:保留原有默认图片(兜底逻辑)

四、项目专属 Maven 运行命令

  1. 运行最简单的入门测试

    mvn test -Dtest=TemplateDesignerServiceTest#rewriteDesignerAssetPathsInJson_replacesDraftIdInUrl

  2. 运行「生成配置文件」某一个测试

    mvn test -Dtest=TemplateDesignerServiceTest#testGenerateRuntimeFilesForDraft_IdPhotoV1

  3. 运行整个测试类(所有9个测试方法)

    mvn test -Dtest=TemplateDesignerServiceTest

五、项目测试核心规则总结

  1. 所有外部依赖一律 Mock

数据库仓库、OSS、第三方服务,全部用 @Mock 伪造,不碰真实环境。

  1. when 造数据,assertEquals 判结果

先规定假对象返回什么数据,再运行代码,最后判断输出对不对。

  1. verify 校验「动作是否执行」

上传文件、保存数据库这类操作,必须用 verify 检查有没有被调用。

  1. 边界场景一定要测

空数据、文件丢失、非法参数、异常类型,保证代码有容错能力。

  1. 工具方法复用

复杂对象、文件、ZIP包,抽取成通用方法,减少重复代码。

六、常见问题排错

  1. 测试报空指针

大概率:忘记加 @ExtendWith(MockitoExtension.class),@Mock 对象没生效。

  1. 断言失败(结果不对)

检查 when() 设定的返回值、业务逻辑是否和预期一致。

  1. 临时文件残留

代码里 finally 块会自动删除临时文件,不用手动清理。

  1. 私有方法无法直接调用

使用 ReflectionTestUtils.invokeMethod 反射调用,专门用来测试类内部私有逻辑。


整体学习路线建议

  1. 先跑上篇基础案例,弄懂 @Mock / when / assertEquals / verify 四大基础用法
  2. 再跑项目里最简单的路径替换测试,熟悉项目代码结构
  3. 逐步学习「生成配置文件」测试,理解模板类型分支逻辑
  4. 最后学习「草稿发布」测试,掌握复杂链路、ZIP文件、OSS模拟、边界场景测试
相关推荐
jnrjian1 小时前
GATHER_FULL_STATS_JOB oracle自动收集统计信息 options => ‘gather‘
oracle
weixin_523185322 小时前
达梦数据库事务机制踩坑:默认不自动提交事务
数据库·oracle
云絮.2 小时前
数据库约束
java·数据库·sql·mysql·oracle
测试员周周16 小时前
【AI测试智能体-面试】AI测试面试60题(附回答思路)
人工智能·python·功能测试·测试工具·单元测试·自动化·测试用例
阿演18 小时前
DataDjinn 新版本更新:新增 Oracle 支持,查询窗口、表预览和连接树继续打磨
数据库·oracle·ai编程·数据库连接工具
lixora18 小时前
Oracle 11g Active Data Guard Go 自动化部署工具 v1.0
数据库·oracle
mN9B2uk1719 小时前
大数据量高并发的数据库优化
服务器·数据库·oracle
蓝鸟197421 小时前
Oracle超大DMP备份文件瘦身、日志精简、磁盘空间优化实战方案日志
数据库·oracle·数据库运维·生产运维实战·oracle避坑·磁盘空间优化·oracle日志清理
asdfg12589631 天前
一文通俗理解JDBC中的核心概念+案例
java·数据库·oracle·jdbc