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计算替换为动态计算方式。

BibiGPT-哔哔终结者
BibiGPT-哔哔终结者

B站视频总结器-一键总结 音视频内容

下载
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这样的高级断言库。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
软件测试常用工具
软件测试常用工具

软件测试常用工具有Selenium、JUnit、Appium、JMeter、LoadRunner、Postman、TestNG、LoadUI、SoapUI、Cucumber和Robot Framework等等。测试人员可以根据具体的测试需求和技术栈选择适合的工具,提高测试效率和准确性 。

439

2023.10.13

java测试工具有哪些
java测试工具有哪些

java测试工具有JUnit、TestNG、Mockito、Selenium、Apache JMeter和Cucumber。php还给大家带来了java有关的教程,欢迎大家前来学习阅读,希望对大家能有所帮助。

300

2023.10.23

Java 单元测试
Java 单元测试

本专题聚焦 Java 在软件测试与持续集成流程中的实战应用,系统讲解 JUnit 单元测试框架、Mock 数据、集成测试、代码覆盖率分析、Maven 测试配置、CI/CD 流水线搭建(Jenkins、GitHub Actions)等关键内容。通过实战案例(如企业级项目自动化测试、持续交付流程搭建),帮助学习者掌握 Java 项目质量保障与自动化交付的完整体系。

19

2025.10.24

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

443

2023.08.02

css中float用法
css中float用法

css中float属性允许元素脱离文档流并沿其父元素边缘排列,用于创建并排列、对齐文本图像、浮动菜单边栏和重叠元素。想了解更多float的相关内容,可以阅读本专题下面的文章。

579

2024.04.28

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

102

2025.10.23

c++怎么把double转成int
c++怎么把double转成int

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

93

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

102

2025.10.23

俄罗斯Yandex引擎入口
俄罗斯Yandex引擎入口

2026年俄罗斯Yandex搜索引擎最新入口汇总,涵盖免登录、多语言支持、无广告视频播放及本地化服务等核心功能。阅读专题下面的文章了解更多详细内容。

158

2026.01.28

热门下载

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

精品课程

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

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.8万人学习

Java 教程
Java 教程

共578课时 | 52.5万人学习

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

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