Mockito测试中服务层更新方法:正确模拟Repository行为

mockito测试中服务层更新方法:正确模拟repository行为

本文探讨了在M%ignore_a_1%ckito单元测试中,当服务层方法依赖于Repository的findById方法返回Optional时,可能遇到的UserNotFoundException问题。核心原因是findById方法未被正确模拟,导致Mockito默认返回Optional.empty()。文章提供了详细的解决方案,即通过when().thenReturn()明确模拟findById的返回值,并讨论了Mockito的默认行为、测试时序以及数据模拟的注意事项,旨在帮助开发者编写更健壮的服务层单元测试。

问题剖析:Optional.isEmpty()引发的异常

在开发基于Spring Boot等框架的应用时,服务层(Service Layer)通常会与数据访问层(Repository Layer)进行交互。当对服务层方法进行单元测试时,我们通常会使用Mockito等框架来模拟(Mock)Repository的行为,以隔离测试范围,确保只测试服务层自身的逻辑。

一个常见的问题场景是,当服务层中的更新方法(例如updateUser)首先通过Repository的findById方法查询现有数据,然后根据查询结果进行后续操作时,单元测试可能会失败并抛出UserNotFoundException。

考虑以下服务层updateUser方法的简化实现:

@Overridepublic UserDTO updateUser(String id, UserDTO updatedUser) {    // 1. 通过 updatedUser 的 userName 字段查找用户    Optional databaseUser = userRepository.findById(Integer.valueOf(updatedUser.getUserName()));    // 2. 如果未找到用户,则抛出异常    if (databaseUser.isEmpty()) {        throw new UserNotFoundException("User with the this id is not found");    }    // 3. 将更新后的 DTO 映射为实体    UserEntity entity = mapToUserEntity(updatedUser);    // 4. 保存更新后的实体    return map(userRepository.save(entity));}

在上述方法中,userRepository.findById()的返回值是一个Optional。如果这个Optional是空的(isEmpty()为true),则会抛出UserNotFoundException。

现在,让我们看看一个可能导致此问题的单元测试:

@Testvoid updateUserTest(){    final int id = 1;    final int userNameAsId = 12; // 假设 updatedUser.userName 会被解析为 12    UserDTO userDto = new UserDTO();    userDto.setUserName(String.valueOf(userNameAsId)); // 此值将用于 findById    // ... 其他 userDto 字段的设置 ...    // 模拟 roleRepository,与当前问题无关    when(roleRepository.findById(any())).thenReturn(Optional.of(new UserDTO().setId(2L)));    // 将 userDto 映射为 UserEntity,此实体将用于 save 方法    UserEntity userEntity = userService.mapToUserEntity(userDto);    // 模拟 userRepository.save 方法    when(userRepository.save(any())).thenReturn(userEntity.setId(id));    // 调用被测试的服务方法    userService.updateUser(String.valueOf(id), userDto); // 第一次调用    var actualUser = userService.updateUser(String.valueOf(id), userDto); // 第二次调用    // ... 断言 ...}

在这个测试中,问题在于 userRepository.findById() 方法没有被显式地模拟。当Mockito遇到一个未被模拟的方法调用时,它会使用其默认行为(RETURNS_DEFAULTS)。对于返回Optional类型的方法,Mockito的默认行为是返回一个空的Optional(即Optional.empty())。因此,当updateUser方法中的userRepository.findById(Integer.valueOf(updatedUser.getUserName()))被调用时,它将返回Optional.empty(),从而触发databaseUser.isEmpty()为true,最终导致UserNotFoundException被抛出,测试失败。

解决方案:正确模拟依赖行为

要解决这个问题,我们需要在调用被测试的服务方法之前,明确地模拟userRepository.findById()的行为,使其返回一个包含有效UserEntity的Optional。

以下是修正后的测试码片段,突出显示了关键的模拟部分:

@Testvoid updateUserTestCorrected() {    final int id = 1;    final int userNameForLookup = 12; // 这个值是 updateUser 方法中 findById 的参数来源    UserDTO userDto = new UserDTO();    userDto.setUserName(String.valueOf(userNameForLookup)); // 确保 DTO 中的 userName 与查找逻辑匹配    userDto.setId(String.valueOf(id));    userDto.setName(new UserDTO.Name("surname", "firstname", "patronymic"));    userDto.setActive(true);    // ... 其他 userDto 字段的设置 ...    // 1. 模拟 userRepository.findById 方法    //    findById 会被调用参数为 userNameForLookup (即 12)    //    我们应该返回一个包含现有 UserEntity 的 Optional    UserEntity existingUserEntity = new UserEntity();    existingUserEntity.setId(userNameForLookup); // 设置一个 ID,表示这是数据库中已存在的用户    existingUserEntity.setName(new UserEntity.Name("oldSurname", "oldFirstname", "oldPatronymic"));    // ... 根据需要设置 existingUserEntity 的其他字段 ...    when(userRepository.findById(userNameForLookup)).thenReturn(Optional.of(existingUserEntity));    // 2. 模拟 userRepository.save 方法    //    这个实体是从 userDto 映射而来,是 save 方法的期望入参    UserEntity entityToSave = userService.mapToUserEntity(userDto);    entityToSave.setId(id); // 确保保存后返回的实体 ID 正确    when(userRepository.save(any(UserEntity.class))).thenReturn(entityToSave);    // 3. 调用被测试的服务方法    UserDTO actualUser = userService.updateUser(String.valueOf(id), userDto);    // 4. 断言验证    // 确保实际返回的用户数据与预期一致    // 注意:原始测试中对 userDto.setUserName(String.valueOf(id)); 的操作应在断言前完成,    // 或确保 userDto 在测试开始时就完全代表期望的最终状态。    // 这里我们直接比较 actualUser 和 userDto 的关键属性。    assertEquals(userDto.getUserName(), actualUser.getUserName());    assertEquals(userDto.getName().getFirstName(), actualUser.getName().getFirstName());    assertEquals(userDto.getId(), actualUser.getId());    // ... 更多详细断言 ...}

通过添加 when(userRepository.findById(userNameForLookup)).thenReturn(Optional.of(existingUserEntity)); 这一行,我们明确告诉Mockito:当userRepository.findById(12)被调用时,返回一个包含existingUserEntity的Optional,从而避免了Optional.empty()的默认行为,使服务层的isEmpty()检查通过。

进阶考量与最佳实践

Mockito默认行为的理解Mockito在没有明确模拟的情况下,对于不同返回类型的方法有不同的默认行为:

原始类型(如int, boolean): 返回其类型的默认值(0, false)。集合类型(如List, Set, Map): 返回空集合。Optional类型: 返回Optional.empty()。对象类型: 返回null。了解这些默认行为对于编写健壮的测试至关重要,它可以帮助我们预判哪些方法需要被显式模拟。

模拟的完整性与时序在单元测试中,模拟所有被测方法所依赖的外部协作对象(Mocks)的行为是关键。确保在调用被测方法(即测试的“执行”阶段)之前,所有相关的模拟都已设置完毕。例如,findById的模拟必须在userService.updateUser被调用之前完成。

模拟数据的匹与上下文

findById的模拟: findById通常用于模拟从数据库中“读取”现有数据。因此,返回的UserEntity应该代表一个“已存在”的用户。它的ID应该与服务方法中用于查找的ID参数匹配。save的模拟: save方法通常用于模拟数据更新或插入。它的入参是服务层准备好的实体,返回的是“保存后”的实体(通常是同一个实体,可能带有数据库生成的ID)。在实际测试中,确保模拟返回的数据能够满足后续业务逻辑的需要,例如,如果后续逻辑会访问existingUserEntity的某个字段,那么模拟时就需要确保该字段有值。

测试断言的重要性一个完整的单元测试不仅要执行代码,更要验证其行为和结果。在updateUserTestCorrected示例中,我们不仅解决了UserNotFoundException,还增加了assertEquals来验证updateUser方法是否返回了预期的UserDTO,以及其内部的字段是否正确更新。这确保了我们不仅测试了代码的执行路径,还测试了其功能正确性。

总结

在Mockito单元测试中,当服务层方法依赖于Repository返回Optional类型的数据时,务必注意显式模拟findById等方法,以避免因Mockito默认返回Optional.empty()而导致的业务逻辑异常。通过清晰地模拟所有外部依赖的行为,并结合对Mockito默认行为的理解,我们可以编写出更稳定、更可靠的服务层单元测试,从而有效提升代码质量。

以上就是Mockito测试中服务层更新方法:正确模拟Repository行为的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/130938.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
失眠的原因及对策(探索又困睡不着的背后原因与解决方法)
上一篇 2025年11月28日 15:03:15
拼多多账号资料修改失败怎么办 拼多多账号资料更新修复技巧
下一篇 2025年11月28日 15:03:24

相关推荐

  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 三星不再独享,消息称搭载骁龙 8 Gen 3 领先版处理器新机即将发布

    三星不再独享,消息称搭载骁龙 8 Gen 3 领先版处理器新机即将发布三星不再独享,消息称搭载骁龙 8 Gen 3 领先版处理器新机即将发布三星不再独享,消息称搭载骁龙 8 Gen 3 领先版处理器新机即将发布三星不再独享,消息称搭载骁龙 8 Gen 3 领先版处理器新机即将发布

    6 月 15 日消息,据博主@肥威 今日爆料,搭载骁龙 8 Gen 3 领先版%ign%ignore_a_1%re_a_1%的新机即将发布,把之前的 for Galaxy 改成“for Everybody”。 Pic Copilot AI时代的顶级电商设计师,轻松打造爆款产品图片 158 查看详情 …

    2026年5月10日 用户投稿
    000
  • 高通预热 2023 骁龙峰会:以AI为主题,10 月 25-26 日举行

    高通预热 2023 骁龙峰会:以AI为主题,10 月 25-26 日举行高通预热 2023 骁龙峰会:以AI为主题,10 月 25-26 日举行高通预热 2023 骁龙峰会:以AI为主题,10 月 25-26 日举行高通预热 2023 骁龙峰会:以AI为主题,10 月 25-26 日举行

    【环球网科技综合报道】10月17日消息,高通今日对 2023 骁龙峰会进行了预热,本次大会将以 %ign%ignore_a_1%re_a_1% 为主题,届时骁龙 8 gen 3 处理器也很大可能在本届峰会亮相。 在临近活动召开之日,相关业内人士也透露了高通骁龙8Gen3跑分及规格。据悉,高通骁龙8 …

    2026年5月10日 用户投稿
    000
  • 如何在不暴露密钥的情况下,在客户端创建 Stripe Payment Link

    本文介绍了在纯静态网站环境下,如何利用 Stripe Payment Link 实现商品售卖,并着重讨论了在不暴露 Stripe 密钥的前提下,客户端创建 Payment Link 的可行性。分析了直接在客户端使用密钥的风险,并提出了预先生成 Payment Link 或使用后端服务动态生成 Pay…

    2026年5月10日
    000
  • php登录怎么实现_php用户登录系统完整实现

    <blockquote>PHP用户登录系统的核心是安全验证与会话管理。首先创建POST提交的登录表单,避免敏感信息暴露;后端通过session_start()启动会话,使用trim()和htmlspecialchars()清理输入,防止XSS攻击;利用PDO预处理语句查询数据库,防止SQ…

    用户投稿 2026年5月10日
    000
  • HTMLAMP怎么做_加速移动页面实现教程

    答案:HTML AMP通过规范标签、禁用自定义JS、引入AMP JS库和缓存技术提升移动页面加载速度,需遵循AMP HTML标准并验证有效性,有助于SEO但非万能,未来将更开放并与PWA等融合。 HTML AMP 旨在加速移动页面加载速度,提升用户体验。简单来说,它通过限制某些 HTML 功能,并采…

    2026年5月10日
    000
  • 使用 C++ 构建高性能服务器架构的最佳实践

    遵循 c++++ 中构建高性能服务器架构的最佳实践可以创建可扩展、可靠且可维护的系统:使用线程池以重用线程,提高性能。利用协程减少上下文切换和内存开销,提升性能。通过智能指针和引用计数优化内存管理,避免内存泄漏和性能瓶颈。选择哈希表、数组和链表等高效的数据结构,优化数据访问和存储。充分利用现代 c+…

    2026年5月10日
    000
  • .NET中的仓储模式(Repository Pattern)是什么?如何解耦业务逻辑和数据访问?

    仓储模式是.NET中用于分离业务逻辑与数据访问的抽象层,通过定义如IUserRepository接口并结合依赖注入,实现对数据访问的具体技术解耦;业务逻辑仅依赖接口,可通过SqlUserRepository等具体实现操作数据库,而无需知晓底层细节;该模式提升可维护性、支持单元测试、降低耦合,并可配合…

    2026年5月10日
    000
  • 一加手机因5G专利纠纷在德国再次被停售

    一加手机因5G专利纠纷在德国再次被停售一加手机因5G专利纠纷在德国再次被停售一加手机因5G专利纠纷在德国再次被停售一加手机因5G专利纠纷在德国再次被停售

    it之家 10 月 2 日消息,科技媒体 android headline 昨日(10 月 1 日)发布博文,报道称一加(oneplus)由于和 interdigital 之间的 5g 技术专利纠纷,其品牌手机再次在德国停售。it之家曾于今年 1 月报道,oppo 和诺基亚达成协议,一加手机重返德国…

    2026年5月10日 用户投稿
    000
  • 掌握Python中嵌套列表与字典的数据访问技巧

    本文详细介绍了在Python中如何高效且准确地访问复杂嵌套数据结构(特别是包含列表和字典的多层JSON数据)中的特定值。通过具体示例,文章解释了直接索引列表元素和字典键的正确方法,避免了常见的类型错误,并提供了处理多条记录和潜在数据缺失的健壮性建议,旨在帮助开发者熟练提取深层数据。 理解嵌套数据结构…

    2026年5月10日
    000
  • Shiny 应用中实现可滚动 Sortable 列表的实践指南

    本文详细介绍了如何在 Shiny 应用中创建具有滚动功能的 sortable 列表。通过应用 CSS 样式 max-height 和 overflow-y: auto 到 rank_list 容器,用户可以有效管理内容过多的列表,确保界面整洁且用户体验良好。教程将提供完整的代码示例和详细解释,帮助开…

    2026年5月10日
    000
  • 指针和数组在C++中有什么区别 内存访问方式与使用场景对比

    指针和数组在C++中有什么区别 内存访问方式与使用场景对比指针和数组在C++中有什么区别 内存访问方式与使用场景对比指针和数组在C++中有什么区别 内存访问方式与使用场景对比指针和数组在C++中有什么区别 内存访问方式与使用场景对比

    指针和数组在c++++中本质不同,使用场景和内存访问方式也存在差异。1. 指针是变量,存储地址,可改变指向;数组是连续内存块,大小固定,不可赋值。2. 数组访问基于固定偏移,编译器直接计算地址;指针访问依赖当前地址,通过移动实现数据访问。3. 数组适合静态结构、保证内存连续的场景,如局部数据存储;指…

    2026年5月10日 用户投稿
    000
  • .NET中的WPF是什么?如何使用MVVM模式来构建桌面应用?

    WPF是.NET的UI框架,使用XAML实现界面与逻辑分离,支持数据绑定、样式模板和MVVM模式,通过ViewModel暴露数据与命令,View绑定其属性与ICommand实现交互,提升可维护性。 WPF(Windows Presentation Foundation)是 .NET 框架中的一个用于…

    2026年5月10日
    000
  • 在Ubuntu 20.04上安装最新版Golang的最佳实践是什么

    最直接且推荐的做法是从官网下载最新Go二进制包并手动配置。首先下载%ignore_a_1%1.22.3.linux-amd64.tar.gz,解压至/usr/local,再将/usr/local/go/bin加入PATH环境变量,并设置GOPATH为$HOME/go,最后通过go version和g…

    2026年5月10日
    000
  • 在点击图片时动态显示其替代文本(Alt Text)的JavaScript教程

    在点击图片时动态显示其替代文本(Alt Text)的JavaScript教程在点击图片时动态显示其替代文本(Alt Text)的JavaScript教程在点击图片时动态显示其替代文本(Alt Text)的JavaScript教程在点击图片时动态显示其替代文本(Alt Text)的JavaScript教程

    本教程详细介绍了如何利用JavaScript在用户点击缩略图时,动态地在大图下方显示其对应的替代文本(Alt Text)。通过修改现有函数,我们能够获取图像的alt属性,并将其内容插入到指定的HTML元素中,从而提升用户体验和信息传达效率。 引言 在网页开发中,图片是不可或缺的元素。为了提升用户体验…

    2026年5月10日 用户投稿
    000
  • 前端基本面20

    前端开发实践:自动完成功能设计与实现 本文探讨如何设计和实现一个高效的前端自动完成功能,并重点关注其架构、API设计、性能优化和用户体验。 1. 数据序列化 (JSON.stringify) 在处理自动完成功能的数据时,JSON.stringify 用于将 JavaScript 对象转换为 JSON…

    2026年5月10日
    000
  • HTML表单如何实现白名单功能?怎样只允许授权用户?

    要实现%ignore_a_1%的白名单功能并确保只有授权用户操作,核心答案是必须依赖后端服务器进行严格的身份认证、会话管理、授权检查和数据验证,前端仅能提供用户体验层面的初步提示而不能保障安全;具体而言,首先通过用户身份认证(如用户名/密码或oauth)确认用户身份,服务器创建会话并返回标识符,后续…

    2026年5月10日
    800
  • Golang反射与标签解析结合使用实例

    Golang反射结合结构体标签的核心优势在于提供运行时动态解析和操作结构体元数据的能力,实现高度灵活、解耦的系统设计。通过reflect.TypeOf(obj).Field(i).Tag.Get(“tag_name”)模式,可在不修改结构体的前提下集中管理JSON序列化、数据…

    2026年5月10日
    300
  • 如何计算C++结构体的大小?解析结构体内存对齐原则

    如何计算C++结构体的大小?解析结构体内存对齐原则如何计算C++结构体的大小?解析结构体内存对齐原则如何计算C++结构体的大小?解析结构体内存对齐原则如何计算C++结构体的大小?解析结构体内存对齐原则

    结构体内存对齐的原则包括:1. 结构体成员对齐,每个成员按自身大小对齐;2. 结构体整体对齐,整体大小需是对齐系数(通常为最大成员大小)的倍数;3. 填充字节插入以满足上述规则。例如,struct mystruct { char a; int b; char c;} 默认情况下会因填充导致大小为12…

    2026年5月10日 用户投稿
    000
  • C#中什么是依赖注入 C# ASP.NET Core依赖注入(DI)的实现原理

    依赖注入是ASP.NET Core实现IoC的核心机制,通过外部容器在运行时将服务实例自动传递给类的构造函数,降低耦合并提升可测试性与维护性。传统方式中类内部直接new依赖导致紧耦合,而DI通过构造函数接收依赖接口,由框架注入具体实现,使业务逻辑与实现分离。ASP.NET Core内置轻量级容器,基…

    2026年5月10日
    000

发表回复

登录后才能评论
关注微信