
本文探讨了在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"。
解决上述问题的核心在于“依赖注入”(Dependency Injection)。与其在方法内部直接创建并使用Random实例,不如将随机数生成的能力作为依赖项“注入”到方法中。这样,在生产代码中可以注入真实的随机数生成器,而在测试代码中则可以注入一个可控的模拟实现。
对于随机数生成,Java 8引入的函数式接口java.util.function.DoubleSupplier是一个理想的选择。它定义了一个抽象方法getAsDouble(),返回一个double类型的值,非常适合作为随机数生成器的抽象。
我们可以通过方法重载来实现这种依赖注入:
以下是修改后的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(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来模拟Random类本身,但由于Random是一个相对较大的类,具有多个方法,直接模拟可能会导致测试代码更加臃肿,需要设置更多的when().thenReturn()规则,并且可能更容易受到Random类内部实现变化的影响。相比之下,模拟DoubleSupplier这种单一职责的接口,其测试代码将更加简洁、健壮和易于维护。
在Java中测试依赖于随机数生成的方法时,直接模拟java.util.Random类并非最佳实践,且可能遇到技术障碍。通过引入方法级依赖注入,并利用DoubleSupplier函数式接口作为随机数生成行为的抽象,我们能够以一种优雅、健壮且高度可控的方式实现单元测试。这种模式不仅提升了代码的可测试性,也促进了更清晰的职责分离和更好的代码维护性,是处理随机性依赖测试的推荐方法。
以上就是Java中随机数生成方法的可测试性:使用依赖注入与DoubleSupplier的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号