讲师中心 微信公众号
AI工具推荐 视频效率加速

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

陌强同学_2652

陌强同学_2652

发布时间:2025-12-04 18:36:41

|

303人浏览过

|

来源于php中文网

原创

java中随机数生成方法的可测试性:使用依赖注入与doublesupplier

本文探讨了在Java中使用`Random.nextDouble()`方法时,如何有效进行单元测试的挑战。针对Mockito无法直接模拟`java.util.Random`类的问题,文章提出了一种基于方法级依赖注入的解决方案。通过引入`DoubleSupplier`函数式接口,并结合方法重载与`@VisibleForTesting`注解,我们能够实现对随机数生成行为的精确控制和模拟,从而编写出稳定且可维护的测试代码,避免了对系统类的直接模拟,提升了代码的可测试性。

引言:测试随机数生成器面临的挑战

在软件开发中,当业务逻辑依赖于随机数生成时,编写可预测且稳定的单元测试会变得复杂。例如,一个方法可能根据Random.nextDouble()的返回值来决定不同的执行路径。直接测试这类方法的问题在于,每次运行时nextDouble()都会产生不同的结果,导致测试结果不确定。

尝试使用流行的模拟框架Mockito来直接模拟java.util.Random类,往往会遇到以下错误:

Mockito cannot mock this class: class java.util.Random.
Mockito can only mock non-private & non-final classes.

尽管java.util.Random类并非final,理论上是可模拟的,但由于其是Java核心库的一部分,且可能涉及内部机制,Mockito在某些环境下可能对其模拟表现出抵抗,或者即使成功模拟,也可能导致测试代码的复杂性和脆弱性增加。这促使我们寻找更健壮、更符合最佳实践的测试策略。

立即学习“Java免费学习笔记(深入)”;

考虑以下示例方法,它根据随机数生成不同的单词:

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实例,不如将随机数生成的能力作为依赖项“注入”到方法中。这样,在生产代码中可以注入真实的随机数生成器,而在测试代码中则可以注入一个可控的模拟实现。

对于随机数生成,Java 8引入的函数式接口java.util.function.DoubleSupplier是一个理想的选择。它定义了一个抽象方法getAsDouble(),返回一个double类型的值,非常适合作为随机数生成器的抽象。

Google calender
Google calender

使用 Maton API 密钥认证,创建、更新、列出和删除 Google Calendar 事件,支持参与者、时区及 Google Meet。

下载

我们可以通过方法重载来实现这种依赖注入:

  1. 原始方法(生产环境调用): 保持原有签名,但在内部调用新的重载方法,并将Random::nextDouble作为DoubleSupplier传递。
  2. 重载方法(测试环境调用): 接收一个DoubleSupplier参数,并使用它来获取随机数。

以下是修改后的foo()方法示例:

import com.google.common.annotations.VisibleForTesting; // 可选,用于文档化

import java.util.Random;
import java.util.function.DoubleSupplier;

public class MyRandomService {

    // 生产环境调用的方法
    public String foo() {
        Random random = new Random();
        return foo(random::nextDouble); // 将Random::nextDouble作为DoubleSupplier传递
    }

    // 测试专用或包私有的重载方法
    @VisibleForTesting // Guava库中的注解,表示此方法可见性是为了测试
    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 Guava库的注解,用于标记那些为了测试目的而放宽了可见性(例如从private变为package-private)的方法。它提供文档说明,表明该方法不应被生产代码直接调用。如果你的项目不使用Guava,可以省略此注解,但保持包私有(或受保护)的可见性以限制其在测试包中的可访问性。
  • 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 java.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. 避免模拟系统类: 避免了直接模拟java.util.Random这类核心Java库类可能带来的复杂性或兼容性问题。DoubleSupplier是一个简单的接口,易于模拟。
  3. 清晰的职责分离: 业务逻辑方法不再负责创建随机数生成器,而是专注于其核心业务逻辑。随机数生成器的提供者是外部的,职责更加清晰。
  4. 易于维护: 当随机数生成逻辑(例如,从Random切换到ThreadLocalRandom)发生变化时,只需要修改无参的foo()方法,而测试代码和核心业务逻辑foo(DoubleSupplier)无需改动。
  5. 文档化: @VisibleForTesting注解(如果使用)清晰地表明了某些方法可见性提升的意图,有助于代码维护和理解。

虽然理论上可以通过一些高级技巧或特定版本的Mockito来模拟Random类本身,但由于Random是一个相对较大的类,具有多个方法,直接模拟可能会导致测试代码更加臃肿,需要设置更多的when().thenReturn()规则,并且可能更容易受到Random类内部实现变化的影响。相比之下,模拟DoubleSupplier这种单一职责的接口,其测试代码将更加简洁、健壮和易于维护。

总结

在Java中测试依赖于随机数生成的方法时,直接模拟java.util.Random类并非最佳实践,且可能遇到技术障碍。通过引入方法级依赖注入,并利用DoubleSupplier函数式接口作为随机数生成行为的抽象,我们能够以一种优雅、健壮且高度可控的方式实现单元测试。这种模式不仅提升了代码的可测试性,也促进了更清晰的职责分离和更好的代码维护性,是处理随机性依赖测试的推荐方法。

热门AI工具

更多
豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

UpDream
UpDream Hot

一款AI视频创作工具,主要用于哔哩哔哩推出的自研AI视频创作工具,适合需要提升相关任务效率的用户。

PixTV
PixTV Hot

PixTV是一款面向AIGC内容创作的AI视频生成工具。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

立刻MV
立刻MV Hot

立刻MV是一款AI文本写作工具,AI 音乐视频(MV)创作工具。

音述AI
音述AI Hot

一款AI音频处理工具,主要用于音述AI是一个以“用声音述说故事”为核心的 AI 音乐创作与声音分享社区,适合需要提升相关任务效率的用户。

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

讯飞智作

讯飞智作是一款AI视频创作工具,AI文本配音工具,数字人课程、营销视频制作。

相关专题

更多
guava包作用
guava包作用

guava是一个java库,增强了java标准库,提供更有效率和易于使用的集合、实用程序、缓存和并发工具。想了解更多guava的相关内容,可以阅读本专题下面的文章。

4811

2024.05.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

3668

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

684

2025.10.23

硬盘接口类型介绍
硬盘接口类型介绍

硬盘接口类型有IDE、SATA、SCSI、Fibre Channel、USB、eSATA、mSATA、PCIe等等。详细介绍:1、IDE接口是一种并行接口,主要用于连接硬盘和光驱等设备,它主要有两种类型:ATA和ATAPI,IDE接口已经逐渐被SATA接口;2、SATA接口是一种串行接口,相较于IDE接口,它具有更高的传输速度、更低的功耗和更小的体积;3、SCSI接口等等。

3108

2023.10.19

PHP接口编写教程
PHP接口编写教程

本专题整合了PHP接口编写教程,阅读专题下面的文章了解更多详细内容。

4629

2025.10.17

php8.4实现接口限流的教程
php8.4实现接口限流的教程

PHP8.4本身不内置限流功能,需借助Redis(令牌桶)或Swoole(漏桶)实现;文件锁因I/O瓶颈、无跨机共享、秒级精度等缺陷不适用高并发场景。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3729

2025.12.29

java接口相关教程
java接口相关教程

本专题整合了java接口相关内容,阅读专题下面的文章了解更多详细内容。

426

2026.01.19

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

3596

2024.01.03

C++运算符基础入门
C++运算符基础入门

本专题详细讲解了C++运算符的类型、语法与使用方法,涵盖算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、条件运算符及其他特殊运算符,并通过代码示例解析优先级与结合性。

0

2026.10.09

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn