新闻中心
J*a中随机数生成方法的可测试性:使用依赖注入与DoubleSupplier

本文探讨了在J*a中使用`Random.nextDouble()`方法时,如何有效进行单元测试的挑战。针对Mockito无法直接模拟`j*a.util.Random`类的问题,文章提出了一种基于方法级依赖注入的解决方案。通过引入`DoubleSupplier`函数式接口,并结合方法重载与`@VisibleForTesting`注解,我们能够实现对随机数生成行为的精确控制和模拟,从而编写出稳定且可维护的测试代码,避免了对系统类的直接模拟,提升了代码的可测试性。
引言:测试随机数生成器面临的挑战
在软件开发中,当业务逻辑依赖于随机数生成时,编写可预测且稳定的单元测试会变得复杂。例如,一个方法可能根据Random.nextDouble()的返回值来决定不同的执行路径。直接测试这类方法的问题在于,每次运行时nextDouble()都会产生不同的结果,导致测试结果不确定。
尝试使用流行的模拟框架Mockito来直接模拟j*a.util.Random类,往往会遇到以下错误:
Mockito cannot mock this class: class j*a.util.Random. Mockito can only mock non-private & non-final classes.
尽管j*a.util.Random类并非final,理论上是可模拟的,但由于其是J*a核心库的一部分,且可能涉及内部机制,Mockito在某些环境下可能对其模拟表现出抵抗,或者即使成功模拟,也可能导致测试代码的复杂性和脆弱性增加。这促使我们寻找更健壮、更符合最佳实践的测试策略。
考虑以下示例方法,它根据随机数生成不同的单词:
public String foo() {
Random random = new Random();
String word = ""; // 初始化word变量
if(random.nextDouble() <= 0.5) {
word += "Hello";
}
if(random.nextDouble() <= 0.7) { // 注意这里可能需要更精细的逻辑来避免重复调用或累加
word += "World";
}
return word;
}我们的目标是能够控制random.nextDouble()的返回值,以便测试foo()方法在特定随机数条件下的行为,例如,当随机数为0.6时,期望返回"World"。
解决方案:方法级依赖注入与DoubleSupplier
解决上述问题的核心在于“依赖注入”(Dependency Injection)。与其在方法内部直接创建并使用Random实例,不如将随机数生成的能力作为依赖项“注入”到方法中。这样,在生产代码中可以注入真实的随机数生成器,而在测试代码中则可以注入一个可控的模拟实现。
对于随机数生成,J*a 8引入的函数式接口j*a.util.function.DoubleSupplier是一个理想的选择。它定义了一个抽象方法getAsDouble(),返回一个double类型的值,非常适合作为随机数生成器的抽象。
我们可以通过方法重载来实现这种依赖注入:
MedPeer科研绘图
生物医学领域的专业绘图解决方案,告别复杂绘图,专注科研创新
166
查看详情
- 原始方法(生产环境调用): 保持原有签名,但在内部调用新的重载方法,并将Random::nextDouble作为DoubleSupplier传递。
- 重载方法(测试环境调用): 接收一个DoubleSupplier参数,并使用它来获取随机数。
以下是修改后的foo()方法示例:
import com.google.common.annotations.VisibleForTesting; // 可选,用于文档化
import j*a.util.Random;
import j*a.util.function.DoubleSupplier;
public class MyRandomService {
// 生产环境调用的方法
public String foo() {
Random random = new Random();
return foo(random::nextDouble); // 将Random::nextDouble作为DoubleSupplier传递
}
// 测试专用或包私有的重载方法
@VisibleForTesting // Gu*a库中的注解,表示此方法可见性是为了测试
String foo(DoubleSupplier randomDoubleSupplier) {
String word = ""; // 确保word变量被初始化
// 第一次获取随机数
double firstRandom = randomDoubleSupplier.getAsDouble();
if(firstRandom <= 0.5) {
word += "Hello";
}
// 第二次获取随机数,如果逻辑需要两次独立的随机数
// 注意:原始问题中的示例在同一条件下调用了两次nextDouble(),这可能不是预期行为。
// 如果需要两次独立随机数,DoubleSupplier应被调用两次。
// 如果是基于第一次结果的累加条件,则不应再次调用。
// 这里假设是两次独立的判断,因此再次调用。
double secondRandom = randomDoubleSupplier.getAsDouble();
if(secondRandom <= 0.7) {
word += "World";
}
return word;
}
}代码说明:
- foo() (无参): 这是外部或生产代码调用的入口点。它负责创建Random实例,并将其nextDouble()方法封装成DoubleSupplier传递给内部的重载方法。
- foo(DoubleSupplier randomDoubleSupplier): 这是实际包含业务逻辑的方法。它不再直接创建Random实例,而是通过传入的randomDoubleSupplier来获取随机数。
-
@VisibleForTesting: 这是一个来自Google Gu*a库的注解,用于标记那些为了测试目的而放宽了
可见性(例如从private变为package-private)的方法。它提供文档说明,表明该方法不应被生产代码直接调用。如果你的项目不使用Gu*a,可以省略此注解,但保持包私有(或受保护)的可见性以限制其在测试包中的可访问性。 - DoubleSupplier: 这是一个简单的函数式接口,只有一个方法getAsDouble(),返回一个double。Mockito可以非常容易地模拟这种单方法接口。
编写可控的测试
有了上述修改,我们现在可以轻松地为foo(DoubleSupplier)方法编写可控的单元测试。我们不再需要模拟Random类,而是模拟DoubleSupplier接口。
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import j*a.util.function.DoubleSupplier;
import static org.mockito.Mockito.when;
public class MyRandomServiceTest {
@Test
void testFooReturnsWorld() {
MyRandomService service = new MyRandomService();
// 模拟DoubleSupplier
DoubleSupplier mockDoubleSupplier = Mockito.mock(DoubleSupplier.class);
// 设置mockDoubleSupplier在第一次调用时返回0.6,第二次调用时返回0.6 (或任何满足条件的值)
// 根据foo方法的实现,如果条件是 <= 0.5 和 <= 0.7,那么0.6会跳过第一个条件,满足第二个条件。
// 如果两次调用nextDouble()是独立的,则需要设置两次返回值。
when(mockDoubleSupplier.getAsDouble())
.thenReturn(0.6) // 第一次调用
.thenReturn(0.6); // 第二次调用
// 调用重载的foo方法进行测试
String result = service.foo(mockDoubleSupplier);
// 验证结果
Assertions.assertEquals("World", result);
// 验证getAsDouble()方法被调用了两次
Mockito.verify(mockDoubleSupplier, Mockito.times(2)).getAsDouble();
}
@Test
void testFooReturnsHello() {
MyRandomService service = new MyRandomService();
DoubleSupplier mockDoubleSupplier = Mockito.mock(DoubleSupplier.class);
// 设置mockDoubleSupplier在第一次调用时返回0.4,第二次调用时返回0.6
when(mockDoubleSupplier.getAsDouble())
.thenReturn(0.4) // 第一次调用,满足 <= 0.5
.thenReturn(0.6); // 第二次调用,不满足 <= 0.7
String result = service.foo(mockDoubleSupplier);
Assertions.assertEquals("Hello", result);
Mockito.verify(mockDoubleSupplier, Mockito.times(2)).getAsDouble();
}
@Test
void testFooReturnsHelloWorld() {
MyRandomService service = new MyRandomService();
DoubleSupplier mockDoubleSupplier = Mockito.mock(DoubleSupplier.class);
// 设置mockDoubleSupplier在第一次调用时返回0.4,第二次调用时返回0.6
when(mockDoubleSupplier.getAsDouble())
.thenReturn(0.4) // 第一次调用,满足 <= 0.5
.thenReturn(0.6); // 第二次调用,满足 <= 0.7
String result = service.foo(mockDoubleSupplier);
Assertions.assertEquals("HelloWorld", result);
Mockito.verify(mockDoubleSupplier, Mockito.times(2)).getAsDouble();
}
@Test
void testFooReturnsEmptyString() {
MyRandomService service = new MyRandomService();
DoubleSupplier mockDoubleSupplier = Mockito.mock(DoubleSupplier.class);
// 设置mockDoubleSupplier在第一次调用时返回0.8,第二次调用时返回0.8
when(mockDoubleSupplier.getAsDouble())
.thenReturn(0.8) // 第一次调用,不满足 <= 0.5
.thenReturn(0.8); // 第二次调用,不满足 <= 0.7
String result = service.foo(mockDoubleSupplier);
Assertions.assertEquals("", result);
Mockito.verify(mockDoubleSupplier, Mockito.times(2)).getAsDouble();
}
}测试代码说明:
- 我们使用Mockito.mock(DoubleSupplier.class)创建了一个DoubleSupplier的模拟对象。
- 通过when(mockDoubleSupplier.getAsDouble()).thenReturn(...),我们可以精确地控制getAsDouble()方法在每次调用时返回的值。thenReturn(val1).thenReturn(val2)可以设置连续调用的返回值。
- 然后,我们调用service.foo(mockDoubleSupplier),并将模拟对象传入。
- 最后,使用Assertions.assertEquals()验证方法返回的结果是否符合预期。
- Mockito.verify(mockDoubleSupplier, Mockito.times(2)).getAsDouble(); 用于验证getAsDouble()方法是否被调用了两次,这有助于确保业务逻辑按照预期获取了随机数。
优势与最佳实践
采用这种方法级依赖注入的策略,带来了多方面的优势:
- 高可测试性: 彻底解耦了业务逻辑与随机数生成器的具体实现,使得测试代码能够完全控制随机数生成行为,从而编写出稳定、可预测的单元测试。
- 避免模拟系统类: 避免了直接模拟j*a.util.Random这类核心J*a库类可能带来的复杂性或兼容性问题。DoubleSupplier是一个简单的接口,易于模拟。
- 清晰的职责分离: 业务逻辑方法不再负责创建随机数生成器,而是专注于其核心业务逻辑。随机数生成器的提供者是外部的,职责更加清晰。
- 易于维护: 当随机数生成逻辑(例如,从Random切换到ThreadLocalRandom)发生变化时,只需要修改无参的foo()方法,而测试代码和核心业务逻辑foo(DoubleSupplier)无需改动。
- 文档化: @VisibleForTesting注解(如果使用)清晰地表明了某些方法可见性提升的意图,有助于代码维护和理解。
虽然理论上可以通过一些高级技巧或特定版本的Mockito来模拟Random类本身,但由于Random是一个相对较大的类,具有多个方法,直接模拟可能会导致测试代码更加臃肿,需要设置更多的when().thenReturn()规则,并且可能更容易受到Random类内部实现变化的影响。相比之下,模拟DoubleSupplier这种单一职责的接口,其测试代码将更加简洁、健壮和易于维护。
总结
在J*a中测试依赖于随机数生成的方法时,直接模拟j*a.util.Random类并非最佳实践,且可能遇到技术障碍。通过引入方法级依赖注入,并利用DoubleSupplier函数式接口作为随机数生成行为的抽象,我们能够以一种优雅、健壮且高度可控的方式实现单元测试。这种模式不仅提升了代码的可测试性,也促进了更清晰的职责分离和更好的代码维护性,是处理随机性依赖测试的推荐方法。
以上就是J*a中随机数生成方法的可测试性:使用依赖注入与DoubleSupplier的详细内容,更多请关注其它相关文章!
# 这是
# 电商商务网站建设
# 如何评价旅行社网站建设
# 互联网营销推广在线观看
# 平阳县建设网站
# 河北贸易网站优化
# 高港区网站建设功能
# 佛山谷歌seo策略
# 携程网站推广
# 网站建设的流程有哪些
# 外贸seo赚钱么
# 目录下
# 见性
# word
# 返回值
# 单元测试
# 是一个
# 文档
# 转换为
# 两次
# 随机数
# 软件开发
# google
# go
# java
相关栏目:
【
科技资讯46185 】
【
网络学院92790 】
相关推荐:
支付宝如何管理隐私设置_支付宝隐私保护的配置技巧
mc.js免安装版 mc.js一键畅玩入口
快手极速版在线观看 官方网页版登录地址
CKEditor 5 自定义构建在React应用中渲染失败的调试与解决
CSS Flexbox如何实现多行排列_flex-wrap wrap自动换行显示
wps文字怎么插入目录并自动更新_wps文字如何插入目录并自动更新方法
如何在Promise链中优雅地中断后续then执行
Lar*el如何生成PDF或Excel文件_Lar*el文档导出工具与使用教程
Flexbox布局实践:实现粘性导航栏与底部固定页脚
小红书商家版怎样在笔记嵌入商品卡路径_小红书商家版在笔记嵌入商品卡路径【挂载教程】
Win10快速启动功能利弊分析 Win10开启或关闭快速启动教程【技巧】
在Blazor WebAssembly应用中动态注入客户端特定指标代码的策略
如何高效处理PHP中的Excel数据导入导出?PortPHP/Spreadsheet助你轻松搞定!
J*a TimerTask中HashMap意外清空的深层原因与解决方案
高德地图家和公司地址在哪设置 高德地图通勤路线设置方法【超详细】
TikTok网页版直接登录 TikTok网页端官方平台入口
文本文档写html代码怎么运行_文本文档html代码运行步骤【教程】
Win11怎么修改默认浏览器_Windows 11设置Chrome为默认
Python多线程中正确使用sigwait处理SIGALRM信号
Win10怎么设置静态IP地址 Win10手动配置IP地址步骤【指南】
Python模块化编程:有效管理依赖与避免循环引用
《GTA6》开发画面疑似泄露!这次可不是AI了
谷歌邮箱注册显示错误Gmail服务器异常与延迟处理
随机参数递归函数的基准调用次数与时间复杂度探究
必由学官网入口 必由学教师登录入口
C++如何打印当前代码行号与文件名_C++预定义宏FILE与LINE的使用
如何在离线环境中使用Composer_Composer离线安装依赖包的技巧与策略
如何将HTML表格多行数据保存到Google Sheet
押井守高度称赞《辐射4》:玩了八年都停不下来!
网易大神怎么保存别人动态的图片_网易大神动态图片保存方法
如何更改在 Excel 中打开超链接时的默认浏览器
天猫双十一预售商品怎么退款_天猫双十一预售退款操作指南
QQ邮箱网页版快速登录 QQ邮箱邮箱账号官方入口地址
钉钉视频会议声音异常如何处理 钉钉会议音频修复技巧
AO3官方在线访问地址 Archive of Our Own最新镜像合集
一加手机拍照效果不好怎么办 一加哈苏影像调校与专业模式使用教程【高手篇】
Python Socket多播通信中指定源IP地址的实践指南
微博网页版主页入口 微博官方网站免登录访问
微信客户端如何收红包_微信客户端接收红包使用教程
微信聊天记录怎么加密_微信聊天记录加密方法
J*a里如何实现线程安全的懒加载单例_懒加载单例实现方法解析
mysql备份恢复性能优化_mysql备份恢复性能优化方法
向日葵客户端怎么进行远程CentOS控制_向日葵客户端远程CentOS控制操作教程
如何修改开机登录密码_Windows账户安全设置超详细教程【必学】
如何为你的Composer包编写自动化测试_集成PHPUnit到Composer的scripts工作流
Golang如何优化CPU绑定任务分配策略_Golang CPU任务分配优化实践
一加 Nord 5 隐私权限异常_一加 Nord 5 系统安全优化
ArrayList与LinkedList操作复杂度详解:遍历与修改
html网页设计源代码怎么运行_运行html网页设计源代码步骤【指南】
J*a 递归快速排序中静态变量的状态管理与陷阱


2025-12-04
浏览次数:次
返回列表
可见性(例如从private变为package-private)的方法。它提供文档说明,表明该方法不应被生产代码直接调用。如果你的项目不使用Gu*a,可以省略此注解,但保持包私有(或受保护)的可见性以限制其在测试包中的可访问性。