首页 > Java > java教程 > 正文

深入理解JUnit浮点数断言:动态Delta值的正确设置与实践

花韻仙語
发布: 2025-09-24 12:03:01
原创
891人浏览过

深入理解JUnit浮点数断言:动态Delta值的正确设置与实践

本教程探讨JUnit中浮点数断言assertEquals的delta参数设置。针对自定义浮点数类型测试,文章指出delta必须为正,并分析了初始设置Math.min(a, b)的局限性。核心内容是推荐使用Math.max(Math.abs(a), Math.abs(b)) / 100作为动态delta,以实现基于相对误差的稳健浮点数比较,并提供示例代码和注意事项。

浮点数比较的挑战与delta参数

软件开发中,尤其是在涉及科学计算、金融或自定义数值类型(如自定义浮点数)时,对浮点数进行精确测试是一个常见的挑战。由于浮点数在计算机内部的表示方式,许多看似简单的算术运算都可能引入微小的精度误差。例如,0.1 + 0.2在某些情况下可能不严格等于0.3。

JUnit框架为解决这一问题提供了assertEquals(String message, Double expected, Double actual, Double delta)方法。其中,delta参数至关重要,它定义了expected和actual之间允许的最大差值。只有当|expected - actual| <= delta时,断言才会通过。正确设置delta是编写健壮浮点数测试的关键。

初始delta设置的误区

在对自定义浮点数类型进行随机值测试时,开发者可能尝试根据输入值动态设置delta。一个常见的误区是使用Math.min(doubles[i], doubles[j])作为delta值,如下面的代码片段所示:

assertEquals("Failed " + doubles[i] + " + " + doubles[j],
             doubles[i] + doubles[j],
             ownFloats[i].add(ownFloats[j]).toDouble(),
             Math.min(doubles[i], doubles[j]));
登录后复制

这种设置方式存在两个主要问题:

  1. delta必须是非负值: assertEquals方法的delta参数预期是一个非负值。如果doubles[i]或doubles[j]中存在负数,Math.min(doubles[i], doubles[j])的结果将是负数,这违反了delta的约定,可能导致意外的行为或测试失败。
  2. delta的代表性不足: 即使输入值都为正,Math.min(doubles[i], doubles[j])可能过小,无法涵盖浮点运算固有的精度误差。例如,当两个较大数相加或相减时,结果的绝对误差可能远大于其中较小输入值的绝对值。这会导致即使计算结果在可接受的误差范围内,测试也可能失败。

考虑以下错误示例:

java.lang.AssertionError: Failed -0.01393084463838419 + -0.01393084463838419 
Expected :-0.02786168927676838
Actual   :-0.027861595153808594
登录后复制

这里,expected和actual之间存在一个微小差异。如果delta被错误地设置为负值或一个不合适的正值,就会导致断言失败。

怪兽AI数字人
怪兽AI数字人

数字人短视频创作,数字人直播,实时驱动数字人

怪兽AI数字人 44
查看详情 怪兽AI数字人

动态delta的正确设置方法

为了克服上述问题,一个更稳健的动态delta设置策略是基于操作数的绝对值和相对误差来确定。推荐的方法是使用Math.max(Math.abs(doubles[i]), Math.abs(doubles[j])) / 100.0。

// 计算预期结果
double expectedAdd = doubles[i] + doubles[j];
// 获取实际结果
double actualAdd = ownFloats[i].add(ownFloats[j]).toDouble();
// 计算动态delta
double deltaAdd = Math.max(Math.abs(doubles[i]), Math.abs(doubles[j])) / 100.0;

assertEquals("Failed " + doubles[i] + " + " + doubles[j],
             expectedAdd, actualAdd, deltaAdd);

// 减法同理
double expectedSub = doubles[i] - doubles[j];
double actualSub = ownFloats[i].sub(ownFloats[j]).toDouble();
double deltaSub = Math.max(Math.abs(doubles[i]), Math.abs(doubles[j])) / 100.0; // 或者根据预期结果的magnitude来调整

assertEquals("Failed " + doubles[i] + " - " + doubles[j],
             expectedSub, actualSub, deltaSub);
登录后复制

这种delta设置方法的优势在于:

  1. 始终为正: Math.abs()确保了delta始终是非负的。
  2. 基于相对误差: Math.max(Math.abs(doubles[i]), Math.abs(doubles[j]))取两个操作数中绝对值较大的一个,这通常能更好地代表参与运算的数值的量级。通过将其除以一个常数(如100.0),我们实际上设置了一个相对误差阈值(例如,1%)。这意味着对于较大的数,delta也会相应变大,允许更大的绝对误差;对于较小的数,delta也会变小,保持相对精度。这对于测试跨度较大的浮点数范围非常有效。
  3. 动态适应: delta不再是固定值,而是根据当前测试用例的输入动态调整,使得测试更加灵活和鲁棒。

完整的测试方法示例

结合上述改进,原始的测试方法可以更新为:

import org.junit.jupiter.api.Test; // 假设使用JUnit 5
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.DoubleStream;

// 假设有一个OwnFloat类,实现了add和sub方法
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 this.value;
    }
}

public class OwnFloatTest {

    @Test
    public void testRandomMath() {
        DoubleStream doubleStream = ThreadLocalRandom.current().doubles(100);

        // 限制double的范围,使其在OwnFloat类可处理的范围内
        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 expectedAdd = doubles[i] + doubles[j];
                double actualAdd = ownFloats[i].add(ownFloats[j]).toDouble();
                // 使用动态delta,基于操作数中绝对值较大的一个
                // 100.0可以根据实际精度要求调整,例如1000.0或更高
                double deltaAdd = Math.max(Math.abs(doubles[i]), Math.abs(doubles[j])) / 100.0;
                // 如果deltaAdd过小(例如接近0),可以设置一个最小delta值,防止除以零或delta过小
                if (deltaAdd < Double.MIN_NORMAL) { // Double.MIN_NORMAL是最小正double值
                    deltaAdd = Double.MIN_NORMAL; 
                }

                assertEquals("Failed " + doubles[i] + " + " + doubles[j],
                             expectedAdd, actualAdd, deltaAdd);

                // 减法测试
                double expectedSub = doubles[i] - doubles[j];
                double actualSub = ownFloats[i].sub(ownFloats[j]).toDouble();
                double deltaSub = Math.max(Math.abs(doubles[i]), Math.abs(doubles[j])) / 100.0;
                if (deltaSub < Double.MIN_NORMAL) {
                    deltaSub = Double.MIN_NORMAL;
                }

                assertEquals("Failed " + doubles[i] + " - " + doubles[j],
                             expectedSub, actualSub, deltaSub);
            }
        }
    }
}
登录后复制

注意事项与最佳实践

  1. delta必须为正: 这是最基本的原则,无论采用何种计算方式,最终的delta值都必须大于等于零。
  2. delta的选取:相对误差 vs. 绝对误差:
    • 绝对误差: 当被测试的浮点数范围很小且已知时,可以使用一个固定的、非常小的正数作为delta(例如1e-9)。
    • 相对误差: 当被测试的浮点数范围很广时(从非常小到非常大),基于相对误差的delta(如Math.max(Math.abs(a), Math.abs(b)) / N)更为合适。N的值需要根据你的精度要求和浮点数实现来调整。
  3. 基于预期结果的delta: 在某些情况下,delta可能更适合基于expected值来计算,例如Math.abs(expected) * relativeErrorTolerance。这可以更好地反映结果本身的精度要求。
  4. ULP (Units in the Last Place): 对于需要极高精度的浮点数比较,可以使用ULP(末位单位)作为误差度量。JUnit 5的Assertions.assertEquals(expected, actual, someUlps)提供了这种功能。ULP表示一个浮点数与下一个可表示的浮点数之间的最小距离,它能更精确地反映浮点数的精度限制。
  5. 避免零点附近的delta问题: 当expected或actual非常接近零时,基于相对误差的delta可能会变得非常小,甚至为零。此时,最好设置一个最小的delta阈值(例如Double.MIN_NORMAL或一个小的固定值),以避免除以零或delta过小导致不必要的失败。
  6. 累积误差: 复杂的浮点运算会累积误差。如果你的自定义浮点数实现涉及多步运算,那么最终结果的误差可能会比单步运算大。因此,对于复杂操作,可能需要设置更大的delta。

总结

正确处理JUnit中的浮点数断言assertEquals的delta参数是确保浮点数代码质量的关键。避免使用可能产生负值的delta,并优先考虑基于操作数或预期结果的相对误差来动态计算delta。通过采纳Math.max(Math.abs(a), Math.abs(b)) / N这种策略,并结合对delta选取原则的理解,可以构建出更健壮、更适应实际浮点数运算特性的单元测试。在追求更高精度时,ULP比较也是一个值得探索的高级选项。

以上就是深入理解JUnit浮点数断言:动态Delta值的正确设置与实践的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号