You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用AssertJ比较含Optional<Instant>的类对象(兼容Java版本精度差异)

问题描述

我有如下Java类:

class Example {
    Instant normal_Instant;
    Optional<Instant> optional_Instant;
}

我想用AssertJ 3.12.2比较该类对象的相等性。由于Java 11提升了Instant的精度,我通过AssertJ的withComparatorForType方法添加了自定义Comparator处理普通Instant字段,但这个方法对包装在Optional中的Instant字段无效。

我自己写了一段代码来解决,但感觉过于繁琐:

public static <T> void assertObjectsWithInstantComparator(T obj1, T obj2) {
    Comparator<Instant> instantComparator = Comparator.comparing(i -> i.truncatedTo(ChronoUnit.MILLIS));

    Assertions.assertThat(obj1)
            .usingRecursiveComparison()
            .withComparatorForType(instantComparator, Instant.class)
            .withComparatorForType(TestUtil::compareOptionalInstants, Optional.class)
            .isEqualTo(obj2);
}

private static int compareOptionalInstants(Optional<Instant> opt1, Optional<Instant> opt2) {
    if (opt1.isPresent() && opt2.isPresent()) {
        Object value1 = opt1.get();
        Object value2 = opt2.get();

        if (value1 instanceof Instant && value2 instanceof Instant) {
            return compareInstants((Instant) value1, (Instant) value2);
        } else {
            return value1.equals(value2) ? 0 : -1;
        }
    } else if (opt1.isPresent()) {
        return 1; // opt1存在但opt2不存在
    } else if (opt2.isPresent()) {
        return -1; // opt2存在但opt1不存在
    } else {
        return 0; // 两个Optional都为空
    }
}

private static int compareInstants(Instant i1, Instant i2) {
    return i1.truncatedTo(ChronoUnit.MILLIS).compareTo(i2.truncatedTo(ChronoUnit.MILLIS));
}

有没有办法专门为包含Instant值的Optional实例注册专属的Comparator?也接受使用Hamcrest等其他库、或者JUnit 5内置实现的方案。

以下是可复现的最小测试示例,测试会因Optional<Instant>字段不匹配失败:

@Test
public void AssertJ_Test() {
    // Java 11中Instant.now()生成纳秒精度的测试值
    Instant normal_instant_test = Instant.parse("2023-06-20T23:45:54.028812Z");
    Optional<Instant> opt_instant_test = Optional.of(Instant.parse("2023-06-20T23:45:54.028812Z"));

    // 从Cassandra获取的Instant是毫秒精度
    Instant normal_instant_DB = Instant.parse("2023-06-20T23:45:54.028Z");
    Optional<Instant> opt_instant_DB = Optional.of(Instant.parse("2023-06-20T23:45:54.028Z"));

    Example test_obj = Example
                    .builder()
                    .normal_Instant(normal_instant_test)
                    .optional_Instant(opt_instant_test)
                    .build();

    Example db_obj = Example
            .builder()
            .normal_Instant(normal_instant_DB)
            .optional_Instant(opt_instant_DB)
            .build();

    Comparator<Instant> instantComparator = Comparator.comparing(i -> i.truncatedTo(ChronoUnit.MILLIS));

    org.assertj.core.api.Assertions.assertThat(db_obj)
            .usingRecursiveComparison()
            .withStrictTypeChecking()
            .withComparatorForType(instantComparator, Instant.class)
            .isEqualTo(test_obj);
}

class Example {
    Instant normal_Instant;
    Optional<Instant> optional_Instant;
}
解决方案

优化AssertJ的处理方式

你可以利用AssertJ递归比较的特性,为Optional类型注册一个通用比较器,让它内部复用已定义的Instant比较逻辑,不用单独写针对Optional<Instant>的判断:

public static <T> void assertObjectsWithInstantComparator(T obj1, T obj2) {
    Comparator<Instant> instantComparator = Comparator.comparing(i -> i.truncatedTo(ChronoUnit.MILLIS));
    
    // 为Optional注册通用比较器,内部复用Instant的比较逻辑
    Comparator<Optional<?>> optionalComparator = (opt1, opt2) -> {
        if (opt1.isPresent() != opt2.isPresent()) {
            return opt1.isPresent() ? 1 : -1;
        }
        if (!opt1.isPresent()) {
            return 0;
        }
        // 拿到Optional中的值,用AssertJ的递归比较器处理(自动复用已注册的Instant比较器)
        return Assertions.assertThat(opt1.get()).usingRecursiveComparison()
                .withComparatorForType(instantComparator, Instant.class)
                .compareTo(opt2.get());
    };

    Assertions.assertThat(obj1)
            .usingRecursiveComparison()
            .withComparatorForType(instantComparator, Instant.class)
            .withComparatorForType(optionalComparator, Optional.class)
            .isEqualTo(obj2);
}

这种方式不仅能处理Optional<Instant>,还能兼容其他类型的Optional字段,只要内部类型的比较器已注册就会自动生效。

自定义AssertJ的TypeComparator(更优雅的方式)

如果想更优雅地处理,可以自定义一个TypeComparator,专门处理Optional类型并递归处理内部值:

public class OptionalInstantAwareComparator extends TypeComparator {
    private final TypeComparator delegate;
    private final Comparator<Instant> instantComparator;

    public OptionalInstantAwareComparator(TypeComparator delegate) {
        this.delegate = delegate;
        this.instantComparator = Comparator.comparing(i -> i.truncatedTo(ChronoUnit.MILLIS));
    }

    @Override
    public boolean canCompare(Object o1, Object o2) {
        return o1 instanceof Optional && o2 instanceof Optional;
    }

    @Override
    public void compare(Object o1, Object o2) throws AssertionError {
        Optional<?> opt1 = (Optional<?>) o1;
        Optional<?> opt2 = (Optional<?>) o2;

        if (opt1.isPresent() != opt2.isPresent()) {
            delegate.compare(o1, o2); // 复用默认Optional比较逻辑处理存在性差异
            return;
        }
        if (!opt1.isPresent()) {
            return;
        }
        // 对内部值使用递归比较,自动应用Instant的自定义比较器
        new RecursiveComparisonAssertionDelegate(opt1.get(), opt2.get())
                .withComparatorForType(instantComparator, Instant.class)
                .areEqual();
    }
}

测试时使用:

Assertions.assertThat(db_obj)
        .usingRecursiveComparison()
        .withComparatorForType(Comparator.comparing(i -> i.truncatedTo(ChronoUnit.MILLIS)), Instant.class)
        .withTypeComparator(new OptionalInstantAwareComparator(new DefaultTypeComparator()))
        .isEqualTo(test_obj);

使用Hamcrest的方案

如果切换到Hamcrest,可以自定义匹配器来处理Optional<Instant>:

public static Matcher<Optional<Instant>> optionalInstantEqualTo(Optional<Instant> expected) {
    return new TypeSafeMatcher<>() {
        @Override
        protected boolean matchesSafely(Optional<Instant> actual) {
            if (actual.isPresent() != expected.isPresent()) {
                return false;
            }
            if (!actual.isPresent()) {
                return true;
            }
            return actual.get().truncatedTo(ChronoUnit.MILLIS)
                    .equals(expected.get().truncatedTo(ChronoUnit.MILLIS));
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("Optional containing Instant equal to ").appendValue(expected);
        }
    };
}

// 针对普通Instant的自定义匹配器
public static Matcher<Instant> instantEqualTo(Instant expected) {
    return new TypeSafeMatcher<>() {
        @Override
        protected boolean matchesSafely(Instant actual) {
            return actual.truncatedTo(ChronoUnit.MILLIS).equals(expected.truncatedTo(ChronoUnit.MILLIS));
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("Instant equal to ").appendValue(expected);
        }
    };
}

测试时调用:

MatcherAssert.assertThat(db_obj.normal_Instant, instantEqualTo(test_obj.normal_Instant));
MatcherAssert.assertThat(db_obj.optional_Instant, optionalInstantEqualTo(test_obj.optional_Instant));

JUnit 5的内置方案

JUnit 5本身没有直接处理精度差异的内置断言,但可以结合org.junit.jupiter.api.Assertions和自定义比较逻辑:

public static void assertInstantEquals(Instant actual, Instant expected) {
    assertEquals(actual.truncatedTo(ChronoUnit.MILLIS), expected.truncatedTo(ChronoUnit.MILLIS));
}

public static void assertOptionalInstantEquals(Optional<Instant> actual, Optional<Instant> expected) {
    assertEquals(actual.isPresent(), expected.isPresent());
    if (actual.isPresent()) {
        assertInstantEquals(actual.get(), expected.get());
    }
}

测试时直接调用:

assertInstantEquals(db_obj.normal_Instant, test_obj.normal_Instant);
assertOptionalInstantEquals(db_obj.optional_Instant, test_obj.optional_Instant);

内容的提问来源于stack exchange,提问作者Ghos3t

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 03:17:19