JUnit测试:如何为assertEquals()正确设置动态delta值?
自定义浮点数类JUnit测试的delta值误区与替代方案
问题描述
为完成作业实现了自定义浮点数类OwnFloat,多数输入组合下运行正常,因此编写了基于随机值的JUnit测试用例。但调用assertEquals(String message, Double expected, Double actual, Double delta)方法时,动态设置delta值遇到问题。
测试代码
@Test public void testRandomMath() { DoubleStream doubleStream = ThreadLocalRandom.current().doubles(100); //limiting the doubles to be withing a workable range for my ownFloat class double[] doubles = 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[doubles.length]; for (int i = 0; i < doubles.length; i++) { ownFloats[i] = new OwnFloat(doubles[i]); } for (int i = 0; i < doubles.length; i++) { for (int j = 0; j < doubles.length; j++) { assertEquals("Failed " + doubles[i] + " + " + doubles[j],doubles[i] + doubles[j], ownFloats[i].add(ownFloats[j]).toDouble(), Math.min(doubles[i], doubles[j])); assertEquals("Failed " + doubles[i] + " - " + doubles[j], doubles[i] - doubles[j], ownFloats[i].sub(ownFloats[j]).toDouble(), Math.min(doubles[i], doubles[j])); } } }
报错信息
java.lang.AssertionError: Failed -0.01393084463838419 + -0.01393084463838419 Expected :-0.02786168927676838 Actual :-0.027861595153808594
回答
delta的理解误区
- delta是允许的绝对误差范围,而非输入值的极值:
assertEquals的delta参数要求是一个非负数值,代表预期值与实际值之间可接受的最大绝对差值。你使用Math.min(doubles[i], doubles[j])作为delta存在两个严重问题:一是当输入为负数时,delta会变成负数,JUnit会直接判定不通过;二是输入值的大小和浮点数运算的误差没有直接关联,这个设置完全不符合误差校验的逻辑。 - 混淆了输入值与浮点数精度的关系:自定义浮点数类的运算误差由其自身的精度设计决定(比如位数、编码方式),和输入值的大小没有直接对应关系,不能用输入值的极值来定义误差范围。
替代delta的可行方案
根据OwnFloat的设计目标,可选择以下几种delta设置方式:
- 基于ULP(末位有效单位):这是浮点数精度的标准衡量方式,
Math.ulp(expected)会返回预期值对应的最小可分辨单位。通常允许1~2个ULP的误差即可覆盖浮点数运算的正常精度损失,示例代码:double sumExpected = doubles[i] + doubles[j]; double sumDelta = 2 * Math.ulp(sumExpected); assertEquals("Failed " + doubles[i] + " + " + doubles[j], sumExpected, ownFloats[i].add(ownFloats[j]).toDouble(), sumDelta); double subExpected = doubles[i] - doubles[j]; double subDelta = 2 * Math.ulp(subExpected); assertEquals("Failed " + doubles[i] + " - " + doubles[j], subExpected, ownFloats[i].sub(ownFloats[j]).toDouble(), subDelta); - 固定精度阈值:如果
OwnFloat是固定小数位数的实现(比如保留6位小数),可以直接设置固定的delta值,例如1e-6,确保实际结果与预期值的差不超过该阈值。 - 相对误差阈值:若
OwnFloat的误差是相对比例型的,可基于预期值的百分比设置delta,比如允许0.0001%的相对误差:double expected = doubles[i] + doubles[j]; double delta = Math.abs(expected) * 1e-6; // 处理预期值为0的特殊情况,回退到极小固定值 if (Double.isNaN(delta) || delta == 0) { delta = 1e-10; } assertEquals("Failed " + doubles[i] + " + " + doubles[j], expected, ownFloats[i].add(ownFloats[j]).toDouble(), delta); - 参考标准浮点数精度:如果
OwnFloat是模拟Java单精度float的实现,可直接使用float对应的精度阈值,比如1e-7(单精度浮点数的有效十进制位数约为7位)。
内容的提问来源于stack exchange,提问作者Lantanar
相关产品推荐
相关产品推荐

