新闻中心

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

2025-12-04
浏览次数:
返回列表

java中随机数生成方法的可测试性:使用依赖注入与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科研绘图 MedPeer科研绘图

生物医学领域的专业绘图解决方案,告别复杂绘图,专注科研创新

MedPeer科研绘图 166 查看详情 MedPeer科研绘图
  1. 原始方法(生产环境调用): 保持原有签名,但在内部调用新的重载方法,并将Random::nextDouble作为DoubleSupplier传递。
  2. 重载方法(测试环境调用): 接收一个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()方法是否被调用了两次,这有助于确保业务逻辑按照预期获取了随机数。

优势与最佳实践

采用这种方法级依赖注入的策略,带来了多方面的优势:

  1. 高可测试性: 彻底解耦了业务逻辑与随机数生成器的具体实现,使得测试代码能够完全控制随机数生成行为,从而编写出稳定、可预测的单元测试。
  2. 避免模拟系统类: 避免了直接模拟j*a.util.Random这类核心J*a库类可能带来的复杂性或兼容性问题。DoubleSupplier是一个简单的接口,易于模拟。
  3. 清晰的职责分离: 业务逻辑方法不再负责创建随机数生成器,而是专注于其核心业务逻辑。随机数生成器的提供者是外部的,职责更加清晰。
  4. 易于维护: 当随机数生成逻辑(例如,从Random切换到ThreadLocalRandom)发生变化时,只需要修改无参的foo()方法,而测试代码和核心业务逻辑foo(DoubleSupplier)无需改动。
  5. 文档化: @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 递归快速排序中静态变量的状态管理与陷阱 

搜索