
在计算机科学中,浮点数(如float和double)的表示方式决定了它们无法精确地表示所有实数。由于内部二进制表示的限制,许多十进制小数在转换为浮点数时会产生微小的误差。这种固有的精度问题导致在比较两个浮点数是否相等时,直接使用==操作符或assertequals(double expected, double actual)方法往往是不可靠的,即使它们在数学上应该是相等的。
为了解决这一问题,JUnit提供了assertEquals(String message, double expected, double actual, double delta)方法。其中,delta参数定义了一个容许的误差范围。如果actual值与expected值的绝对差小于或等于delta,则断言通过。其核心逻辑可以表示为:|expected - actual| <= delta。正确设置delta值是编写可靠浮点数测试的关键。
在许多场景中,被测试的浮点数可能具有非常大的数值范围。一个固定的delta值对于所有测试用例可能并不适用:
因此,根据被比较数值的大小动态调整delta值是更健壮的测试策略。
在设置delta时,开发者常犯以下错误:
为了在JUnit中正确且灵活地处理浮点数断言,推荐采用基于被比较数值量级的动态delta策略。这种策略兼顾了绝对误差和相对误差,适用于广泛的数值范围。
一种有效的动态delta计算方法是: double delta = Math.max(Math.abs(expected), Math.abs(actual)) / N;
这里:
以下示例展示了如何在自定义浮点数类的JUnit测试中应用动态delta策略:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.DoubleStream;
// 假设这是一个自定义的浮点数类,用于模拟浮点数运算
// 实际实现会更复杂,这里仅为示例提供骨架
class OwnFloat {
private double value;
public OwnFloat(double value) {
this.value = value;
}
// 模拟加法运算,可能存在精度损失
public OwnFloat add(OwnFloat other) {
// 实际的OwnFloat实现会在这里进行复杂的自定义浮点数加法逻辑
// 这里简化为直接的双精度浮点数加法
return new OwnFloat(this.value + other.value);
}
// 模拟减法运算,可能存在精度损失
public OwnFloat sub(OwnFloat other) {
// 实际的OwnFloat实现会在这里进行复杂的自定义浮点数减法逻辑
// 这里简化为直接的双精度浮点数减法
return new OwnFloat(this.value - other.value);
}
public double toDouble() {
return this.value;
}
}
public class FloatingPointAssertionTutorial {
@Test
public void testRandomMathWithDynamicDelta() {
// 生成100个随机双精度浮点数
DoubleStream doubleStream = ThreadLocalRandom.current().doubles(100);
// 对生成的双精度数进行处理,使其具有正负、不同量级
double[] testDoubles = doubleStream.map(d -> {
// 随机生成正负数
if (ThreadLocalRandom.current().nextBoolean()) {
return d * -1d;
} else {
return d;
}
}).map(d -> d * Math.pow(2, ThreadLocalRandom.current().nextInt(-8, 9))) // 扩大数值范围,使其具有不同量级
.toArray();
OwnFloat[] ownFloats = new OwnFloat[testDoubles.length];
for (int i = 0; i < testDoubles.length; i++) {
ownFloats[i] = new OwnFloat(testDoubles[i]);
}
// 对所有可能的组合进行加法和减法测试
for (int i = 0; i < testDoubles.length; i++) {
for (int j = 0; j < testDoubles.length; j++) {
// --- 测试加法 ---
double expectedAdd = testDoubles[i] + testDoubles[j];
double actualAdd = ownFloats[i].add(ownFloats[j]).toDouble();
// 动态计算delta值:基于预期值和实际值的最大绝对值,并除以一个精度常数(例如100)
// 这样delta会根据数值的量级自适应调整
double deltaAdd = Math.max(Math.abs(expectedAdd), Math.abs(actualAdd)) / 100.0;
// 特殊处理:如果预期值和实际值都非常接近0,导致deltaAdd也为0,
// 则设置一个非常小的正数作为delta的下限,以避免断言失败(delta为0意味着必须精确相等)
if (deltaAdd == 0.0) {
deltaAdd = 1e-9; // 设置一个最小的绝对误差,例如10的-9次方
}
assertEquals(expectedAdd, actualAdd, deltaAdd,
"Addition Failed for: " + testDoubles[i] + " + " + testDoubles[j]);
// --- 测试减法 ---
double expectedSub = testDoubles[i] - testDoubles[j];
double actualSub = ownFloats[i].sub(ownFloats[j]).toDouble();
// 动态计算delta值(同加法逻辑)
double deltaSub = Math.max(Math.abs(expectedSub), Math.abs(actualSub)) / 100.0;
if (deltaSub == 0.0) {
deltaSub = 1e-9; // 设置一个最小的绝对误差
}
assertEquals(expectedSub, actualSub, deltaSub,
"Subtraction Failed for: " + testDoubles[i] + " - " + testDoubles[j]);
}
}
}
}代码说明:
在JUnit中进行浮点数断言时,正确设置delta参数是确保测试健壮性和有效性的关键。我们了解到delta必须是一个正数,并且静态delta往往不足以应对不同量级的数值。通过采用基于Math.max(Math.abs(expected), Math.abs(actual)) / N的动态delta策略,并结合对接近零数值的特殊处理,可以显著提高浮点数测试的准确性和可靠性。开发者应根据其应用场景和精度要求,灵活选择合适的N值,并始终牢记浮点数运算的近似特性。
以上就是JUnit浮点数断言:动态delta值的正确设置与实践的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号