
本文探讨了在JUnit测试中比较浮点数时,如何正确地动态设置assertEquals方法的delta参数。针对浮点数计算固有的精度问题,文章分析了delta参数的原理与常见误区,并提供了一种基于被比较数值大小的动态delta计算策略,以确保测试的健壮性和准确性。
在软件开发中,尤其是在处理科学计算、金融应用或自定义浮点类型时,对浮点数进行单元测试是至关重要的。然而,由于浮点数(float和double)在计算机内部的表示方式,它们往往无法精确表示所有实数,这导致了浮点数运算结果可能存在微小的误差。因此,在JUnit等测试框架中,直接使用assertEquals(expected, actual)来比较两个浮点数通常是不可靠的,因为它要求两者严格相等。为了解决这个问题,JUnit提供了assertEquals(String message, Double expected, Double actual, Double delta)方法,允许我们指定一个delta(容差)值。
assertEquals中delta参数的作用与误区
delta参数定义了expected和actual之间允许的最大差值。如果|expected – actual| <= delta,则断言通过。理解delta的关键点在于:
delta必须是一个正数:它代表一个允许的误差范围。如果delta为负数或零,则可能导致断言行为异常或不符合预期。例如,assertEquals(0.1, 0.1000000001, 0.0)将失败,因为它们不严格相等。delta的选择至关重要:一个固定的、很小的delta值(例如1e-9)在某些情况下可能有效,但在处理数量级差异很大的浮点数时,它可能不够灵活。对于非常大的数,1e-9可能太小,导致即使结果在合理范围内也失败;对于非常小的数,1e-9可能又太大,导致应该失败的测试通过。
原始问题中,测试者尝试使用Math.min(doubles[i], doubles[j])作为delta。这种做法存在明显的问题:
Math.min(doubles[i], doubles[j])的结果可能是负数,这与delta必须为正的要求相悖。即使结果为正,它也可能不代表一个合理的误差范围,尤其是在被比较的数值本身很小或很大时。
动态设置delta的正确策略
为了解决delta值固定带来的问题,一种更健壮的方法是根据被比较数值的相对大小来动态计算delta。这意味着delta不再是一个固定值,而是与expected或actual的量级相关联。
一个有效的动态delta计算策略是:
Math.max(Math.abs(expected), Math.abs(actual)) / N
其中:
Math.abs(expected)和Math.abs(actual):获取预期值和实际值的绝对值。Math.max(…):取两者中较大的绝对值。这样做是为了确保delta能够覆盖到较大数值可能产生的误差。N:一个可调整的因子,用于控制相对误差的比例。例如,如果N为100,则delta大约是被比较数值中较大者绝对值的1%。这个N值需要根据你的应用对精度的要求进行调优。对于大多数科学计算,N可能是一个更大的数,如1e5到1e10,以获得更高的精度。
为什么这种方法更健壮?这种方法本质上引入了一个相对误差的概念。当被比较的数值很大时,delta也会相应变大,允许更大的绝对误差;当数值很小时,delta也会相应变小,要求更高的绝对精度。这比使用固定delta更能适应各种数量级的浮点数比较。
实践示例:JUnit测试代码优化
以下是根据上述策略优化后的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;// 假设 OwnFloat 是你自定义的浮点数类,并实现了 toDouble() 方法class OwnFloat { private double value; public OwnFloat(double value) { this.value = value; } public OwnFloat add(OwnFloat other) { // 示例实现,实际应包含自定义浮点数的加法逻辑 return new OwnFloat(this.value + other.value); } public OwnFloat sub(OwnFloat other) { // 示例实现,实际应包含自定义浮点数的减法逻辑 return new OwnFloat(this.value - other.value); } public double toDouble() { return value; }}public class OwnFloatMathTest { @Test public void testRandomMath() { DoubleStream doubleStream = ThreadLocalRandom.current().doubles(100); // 限制双精度浮点数在自定义浮点数类可处理的范围内 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++) { double expectedSum = doubles[i] + doubles[j]; double actualSum = ownFloats[i].add(ownFloats[j]).toDouble(); // 动态计算 delta,使用相对误差策略 double deltaSum = Math.max(Math.abs(expectedSum), Math.abs(actualSum)) / 1000.0; // N=1000,可根据精度需求调整 if (deltaSum == 0.0) { // 处理预期值为0的情况,避免除以N后delta仍为0导致严格相等 deltaSum = 1e-10; // 或者一个非常小的固定值 } assertEquals("Failed " + doubles[i] + " + " + doubles[j], expectedSum, actualSum, deltaSum); double expectedSub = doubles[i] - doubles[j]; double actualSub = ownFloats[i].sub(ownFloats[j]).toDouble(); // 动态计算 delta double deltaSub = Math.max(Math.abs(expectedSub), Math.abs(actualSub)) / 1000.0; // N=1000 if (deltaSub == 0.0) { deltaSub = 1e-10; } assertEquals("Failed " + doubles[i] + " - " + doubles[j], expectedSub, actualSub, deltaSub); } } }}
代码说明:
我们将delta的计算从原始的Math.min(…)改为了Math.max(Math.abs(expected), Math.abs(actual)) / N的形式。这里的N取值为1000.0,这意味着我们允许大约千分之一的相对误差。这个值应根据你的OwnFloat实现所能达到的精度以及业务需求来调整。如果你的OwnFloat精度很高,N可以取更大的值(例如1e7或1e10)。特别注意:当expected和actual都为0.0时,Math.max(Math.abs(0.0), Math.abs(0.0))结果为0.0,如果此时delta也为0.0,则assertEquals会要求严格相等。为了避免这种情况,我们增加了一个条件判断:如果计算出的delta为0.0,则将其设置为一个非常小的固定值(如1e-10),以允许对零值进行微小误差的比较。
注意事项与最佳实践
delta值的选择:
绝对误差:assertEquals(expected, actual, fixedDelta)适用于被比较数值的量级相对固定,或者你对误差有一个明确的绝对上限的场景。相对误差:assertEquals(expected, actual, Math.max(Math.abs(expected), Math.abs(actual)) / N)适用于被比较数值的量级变化范围很大,需要自适应误差范围的场景。这是更通用的方法。ULP (Units in the Last Place):对于最高精度的浮点数比较,可以使用Math.ulp(double d)来获取d的最小可表示单位。一些高级断言库提供了基于ULP的比较,这比固定或相对delta更为精确。
N值的调整:在相对误差策略中,N的选取直接影响测试的严格性。你需要根据你的浮点数实现(例如OwnFloat的内部精度)和业务需求,通过实验找到一个合适的N值。如果测试频繁失败,可能需要适当增大delta(即减小N);如果测试过于宽松,可能需要减小delta(即增大N)。
其他断言库的替代方案:
AssertJ:一个流行的Java断言库,提供了更流畅和功能强大的浮点数比较方法,如assertThat(actual).isCloseTo(expected, within(delta))或assertThat(actual).isCloseTo(expected, offset(delta))。它还支持基于ULP的比较,例如isCloseTo(expected, withinPercentage(percentage))或isCloseTo(expected, offset(ulp))。这些库通常能更好地处理浮点数比较的边缘情况,并提供更清晰的错误信息。
何时考虑BigDecimal:
如果你的应用对精度有极高的要求,尤其是在金融计算中,即使是微小的浮点误差也无法接受,那么应考虑使用java.math.BigDecimal类。BigDecimal提供了任意精度的十进制运算,可以完全避免浮点数的精度问题,但其性能开销通常高于double。
总结
正确地处理浮点数比较是编写健壮单元测试的关键。在JUnit中使用assertEquals时,动态设置delta参数,特别是采用基于相对误差的策略(如Math.max(Math.abs(expected), Math.abs(actual)) / N),能够有效地应对浮点数计算固有的精度问题和数值量级的变化。同时,了解delta参数的原理、避免常见误区,并结合实际应用场景调整N值,是确保测试准确性和可靠性的重要步骤。对于需要更高精度或更灵活断言的场景,可以考虑使用BigDecimal或像AssertJ这样的高级断言库。
以上就是JUnit中浮点数断言:动态设置assertEquals的delta参数的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/63183.html
微信扫一扫
支付宝扫一扫