如何用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
相关产品推荐
相关产品推荐

