0

0

JUnit中浮点数断言:动态设置assertEquals的delta参数

聖光之護

聖光之護

发布时间:2025-09-24 11:51:21

|

403人浏览过

|

来源于php中文网

原创

junit中浮点数断言:动态设置assertequals的delta参数

本文探讨了在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|

  1. delta必须是一个正数:它代表一个允许的误差范围。如果delta为负数或零,则可能导致断言行为异常或不符合预期。例如,assertEquals(0.1, 0.1000000001, 0.0)将失败,因为它们不严格相等。
  2. 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计算替换为动态计算方式。

Build AI
Build AI

为您的业务构建自己的AI应用程序。不需要任何技术技能。

下载
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),以允许对零值进行微小误差的比较。

注意事项与最佳实践

  1. 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更为精确。
  2. N值的调整:在相对误差策略中,N的选取直接影响测试的严格性。你需要根据你的浮点数实现(例如OwnFloat的内部精度)和业务需求,通过实验找到一个合适的N值。如果测试频繁失败,可能需要适当增大delta(即减小N);如果测试过于宽松,可能需要减小delta(即增大N)。

  3. 其他断言库的替代方案

    • AssertJ:一个流行的Java断言库,提供了更流畅和功能强大的浮点数比较方法,如assertThat(actual).isCloseTo(expected, within(delta))或assertThat(actual).isCloseTo(expected, offset(delta))。它还支持基于ULP的比较,例如isCloseTo(expected, withinPercentage(percentage))或isCloseTo(expected, offset(ulp))。
    • 这些库通常能更好地处理浮点数比较的边缘情况,并提供更清晰的错误信息。
  4. 何时考虑BigDecimal

    • 如果你的应用对精度有极高的要求,尤其是在金融计算中,即使是微小的浮点误差也无法接受,那么应考虑使用java.math.BigDecimal类。BigDecimal提供了任意精度的十进制运算,可以完全避免浮点数的精度问题,但其性能开销通常高于double。

总结

正确地处理浮点数比较是编写健壮单元测试的关键。在JUnit中使用assertEquals时,动态设置delta参数,特别是采用基于相对误差的策略(如Math.max(Math.abs(expected), Math.abs(actual)) / N),能够有效地应对浮点数计算固有的精度问题和数值量级的变化。同时,了解delta参数的原理、避免常见误区,并结合实际应用场景调整N值,是确保测试准确性和可靠性的重要步骤。对于需要更高精度或更灵活断言的场景,可以考虑使用BigDecimal或像AssertJ这样的高级断言库。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

804

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

722

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

727

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

395

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

445

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

428

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16861

2023.08.03

vlookup函数使用大全
vlookup函数使用大全

本专题整合了vlookup函数相关 教程,阅读专题下面的文章了解更多详细内容。

28

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.1万人学习

C# 教程
C# 教程

共94课时 | 5.6万人学习

Java 教程
Java 教程

共578课时 | 39.7万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

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