0

0

理解与应用Mockito:为何模拟仓库无法保存数据及其解决方案

DDD

DDD

发布时间:2025-10-03 17:42:01

|

646人浏览过

|

来源于php中文网

原创

理解与应用Mockito:为何模拟仓库无法保存数据及其解决方案

在使用 Spring Boot 进行单元测试时,模拟(Mock)的仓库(Repository)并不会真正执行数据持久化操作,因此直接调用 save() 方法无法使数据被“保存”或查询到。本文将深入探讨 Mockito 模拟对象的行为机制,解释为何会出现此类问题,并提供通过 Mockito.when().thenReturn() 明确定义模拟行为的解决方案,确保测试能够有效验证业务逻辑,而无需依赖实际数据库。

深入理解 Mockito 模拟对象

在单元测试中,我们经常使用 mockito 等框架来模拟依赖项,以便将测试范围限制在被测单元本身,实现测试的隔离性、可重复性和执行效率。然而,模拟对象(mock object)与真实对象有着本质的区别

  1. 非实际执行者: 模拟对象并不会执行其真实对应类中的任何业务逻辑。例如,一个模拟的 UserRepository 不会连接数据库、执行 SQL 语句或实际存储数据。
  2. 行为定义: 模拟对象的行为需要被显式地定义。默认情况下,如果不对模拟对象的方法进行行为定义,它们会返回 Java 类型的默认值(例如,null 对于对象,0 对于 int,false 对于 boolean,空集合对于集合类型)。
  3. 状态无记忆: 模拟对象通常不具备状态记忆能力。即使你调用了模拟对象的 save() 方法,它也不会“记住”你传入的数据。因此,后续的 findAll() 或 findById() 调用将无法获取到之前“保存”的数据,除非你明确地定义了这些方法的返回行为。

模拟仓库无法保存数据的根源

当你在单元测试中声明一个 @Mock 的 UserRepository 并尝试调用 repository.save(appUser); 时,实际上并没有任何数据被持久化。这个 save() 调用只是在模拟对象上发生了一个方法调用,但它不会触发任何底层的数据存储机制。因此,当你随后尝试通过 userService.loadUserByUsername(appUser.getUsername()); 调用业务逻辑时,如果该业务逻辑内部依赖 repository.searchByUserName() 来获取用户,而你没有为这个模拟方法定义返回值,它将返回 Optional.empty(),从而导致 UsernameNotFoundException。

即使你在测试配置中包含了 H2 内存数据库的设置,这只是为 Spring Boot 应用程序提供了潜在的真实数据源配置。但在 AppUserServiceTest 中,由于 UserRepository 被 @Mock 注解,UserService 接收到的是一个模拟实例,而不是一个与 H2 数据库交互的真实 UserRepository 实例。

定义模拟行为:Mockito.when().thenReturn()

为了让模拟对象在特定方法被调用时返回预期的结果,我们需要使用 Mockito.when().thenReturn() 语法来定义其行为。这告诉 Mockito:当模拟对象的某个方法以特定参数被调用时,应该返回什么。

考虑以下测试场景:UserService 依赖 UserRepository 的 searchByUserName 方法来查找用户。为了测试 loadUserByUsername 方法的正确性,我们需要模拟 searchByUserName 的行为。

百度文心百中
百度文心百中

百度大模型语义搜索体验中心

下载
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.Optional;

// 假设 AppUser 和 ApplicationRole 是你的实体和枚举类
// import your.package.AppUser;
// import your.package.ApplicationRole;
// import your.package.UserRepository;
// import your.package.UserService;

@ExtendWith(MockitoExtension.class)
@SpringBootTest // 注意:对于纯单元测试,通常不需要 @SpringBootTest,因为它会加载完整的应用上下文,增加测试时间。
                // 如果仅测试 UserService 逻辑,移除此注解可以提高效率。
class AppUserServiceTest {

    @Mock
    private UserRepository repository; // 模拟 UserRepository
    private UserService userService;

    @BeforeEach
    void setUp() {
        // 注入模拟的 repository 到 userService
        userService = new UserService(repository);
    }

    @Test
    void itShouldLoadUsernameByName() {
        // 1. 准备测试数据
        AppUser appUser = new AppUser(
                ApplicationRole.USER,
                "leonardo",
                "rossi",
                "leo__",
                "email@example.com", // 修正 email 格式
                "password"
        );

        // 2. 定义模拟行为:当 repository.searchByUserName() 被调用时,返回包含 appUser 的 Optional
        // 这里使用 any() 匹配器表示任何字符串参数都会触发此行为。
        // 如果需要匹配特定参数,可以直接传入 appUser.getUsername()。
        when(repository.searchByUserName(any(String.class))).thenReturn(Optional.of(appUser));

        // 3. 执行被测方法
        UserDetails foundUser = userService.loadUserByUsername(appUser.getUsername());

        // 4. 验证结果
        // 验证 repository.searchByUserName 方法是否被调用,且参数正确
        verify(repository).searchByUserName(appUser.getUsername());

        // 进一步断言返回的用户详情是否与预期一致
        // 例如:
        // assertThat(foundUser.getUsername()).isEqualTo(appUser.getUsername());
        // assertThat(foundUser.getAuthorities()).containsExactlyInAnyOrderElementsOf(appUser.getAuthorities());
    }

    @Test
    void itShouldThrowExceptionWhenUserNotFound() {
        // 1. 准备数据
        String nonExistentUsername = "nonexistent";

        // 2. 定义模拟行为:当 repository.searchByUserName() 被调用时,返回空的 Optional
        when(repository.searchByUserName(nonExistentUsername)).thenReturn(Optional.empty());

        // 3. 验证异常是否抛出
        org.junit.jupiter.api.Assertions.assertThrows(UsernameNotFoundException.class, () -> {
            userService.loadUserByUsername(nonExistentUsername);
        });

        // 4. 验证 repository.searchByUserName 方法是否被调用
        verify(repository).searchByUserName(nonExistentUsername);
    }
}

在上述修正后的测试代码中:

  • when(repository.searchByUserName(any(String.class))).thenReturn(Optional.of(appUser)); 是核心。它告诉 Mockito,当 repository 对象的 searchByUserName 方法被调用,且传入的是任意 String 类型参数时,就返回一个包含 appUser 的 Optional 对象。
  • any(String.class) 是 Mockito 提供的一个参数匹配器,用于匹配任何 String 类型的参数。如果你希望只在特定参数被调用时返回特定值,可以直接传入具体的参数,例如 when(repository.searchByUserName(appUser.getUsername())).thenReturn(Optional.of(appUser));。
  • verify(repository).searchByUserName(appUser.getUsername()); 用于验证 repository 的 searchByUserName 方法是否确实被 userService 调用了,并且传入的参数是预期的 appUser.getUsername()。

注意事项与最佳实践

  1. 单元测试与集成测试的区别: 模拟对象主要用于单元测试,旨在隔离被测单元。如果你需要测试与数据库的实际交互(例如,确保 JPA 映射正确、事务行为符合预期),那么你需要进行集成测试,此时应使用真实的 UserRepository 实例(可能配合 @DataJpaTest)。
  2. 避免过度模拟: 并非所有依赖都需要模拟。对于简单的值对象或工具类,直接使用真实实例可能更简单。只有当依赖项是外部服务、数据库、复杂组件或耗时操作时,才考虑模拟。
  3. 清晰的模拟行为定义: 确保你的 when().thenReturn() 语句清晰地反映了业务逻辑在不同场景下的预期行为(例如,找到用户、找不到用户、抛出异常等)。
  4. 使用参数匹配器: any(), eq(), argThat() 等匹配器非常有用,但要谨慎使用 any(),因为它可能掩盖测试中参数传递的错误。在可能的情况下,尽量使用 eq() 或直接传入具体值。
  5. 静态导入: 为了代码简洁性,通常会静态导入 org.mockito.Mockito.when 和 org.mockito.ArgumentMatchers.any。

通过正确理解和运用 Mockito 的模拟机制,我们可以编写出高效、可靠且易于维护的单元测试,确保业务逻辑的正确性。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

868

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

745

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

741

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

440

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

447

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

431

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16948

2023.08.03

Python 自然语言处理(NLP)基础与实战
Python 自然语言处理(NLP)基础与实战

本专题系统讲解 Python 在自然语言处理(NLP)领域的基础方法与实战应用,涵盖文本预处理(分词、去停用词)、词性标注、命名实体识别、关键词提取、情感分析,以及常用 NLP 库(NLTK、spaCy)的核心用法。通过真实文本案例,帮助学习者掌握 使用 Python 进行文本分析与语言数据处理的完整流程,适用于内容分析、舆情监测与智能文本应用场景。

2

2026.01.27

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.9万人学习

C# 教程
C# 教程

共94课时 | 7.7万人学习

Java 教程
Java 教程

共578课时 | 51.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号