如何比较属性相似的不同类对象列表?Hamcrest工具是否适用?
当然可以用Hamcrest来解决这个问题!你现在的循环断言方式虽然能工作,但不够简洁,而且当断言失败时,很难快速定位到具体是哪个元素的属性不匹配。Hamcrest提供了更优雅的方案,下面给你两种常用的实现方式:
方法1:自定义Hamcrest匹配器
因为Something和GsonSomething是不同的类,我们可以创建一个自定义匹配器,用来校验GsonSomething实例是否和Something的属性一致。
首先定义匹配器类:
import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; public class GsonSomethingMatcher extends TypeSafeMatcher<GsonSomething> { private final Something expected; public GsonSomethingMatcher(Something expected) { this.expected = expected; } @Override protected boolean matchesSafely(GsonSomething actual) { // 逐一对比属性,可根据需求扩展更多字段 return actual.id == expected.id && actual.name.equals(expected.name); } @Override public void describeTo(Description description) { // 定义失败时的描述信息,方便排查问题 description.appendText("GsonSomething实例应满足:id=") .appendValue(expected.id) .appendText(",name=") .appendValue(expected.name); } }
然后在测试方法中使用containsInAnyOrder:
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import java.util.stream.Collectors; @Test public void testListsMatch() { List<Something> somethingList = ...; // 你的预期列表 List<GsonSomething> gsonSomethingList = ...; // 实际返回的列表 // 将每个Something转换成对应的匹配器 var matchers = somethingList.stream() .map(GsonSomethingMatcher::new) .collect(Collectors.toList()); // 断言两个列表的元素(不考虑顺序)属性完全匹配 assertThat(gsonSomethingList, containsInAnyOrder(matchers)); }
这种方式的好处是:失败时会直接告诉你哪个元素不符合预期,比如“Expected: iterable containing [GsonSomething实例应满足:id=1,name=one, GsonSomething实例应满足:id=2,name=two],但实际元素中缺少xxx”,定位问题非常快。
方法2:转换类实例后对比
如果两个类的属性完全一致,你也可以把其中一个列表转换成另一个类的实例,再用Hamcrest的内置匹配器对比:
步骤1:实现转换方法
private Something convertToSomething(GsonSomething gsonObj) { Something something = new Something(); something.id = gsonObj.id; something.name = gsonObj.name; // 其他属性逐一赋值 return something; }
步骤2:转换后对比
如果你已经重写了Something的equals和hashCode方法,可以直接用containsInAnyOrder:
@Test public void testListsByConversion() { List<Something> expectedList = ...; List<GsonSomething> actualGsonList = ...; // 把实际列表转换成Something类型 List<Something> actualList = actualGsonList.stream() .map(this::convertToSomething) .collect(Collectors.toList()); // 断言元素(不考虑顺序)完全匹配 assertThat(actualList, containsInAnyOrder(expectedList.toArray(new Something[0]))); }
如果不想重写equals方法,可以用Hamcrest的samePropertyValuesAs匹配器(需要引入hamcrest-bean依赖):
<!-- Maven依赖 --> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-bean</artifactId> <version>2.2</version> <scope>test</scope> </dependency>
然后这样使用:
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs; @Test public void testListsWithBeanMatcher() { List<Something> expectedList = ...; List<GsonSomething> actualGsonList = ...; List<Something> actualList = actualGsonList.stream() .map(this::convertToSomething) .collect(Collectors.toList()); // 用属性匹配器对比每个实例 var matchers = expectedList.stream() .map(samePropertyValuesAs::new) .collect(Collectors.toList()); assertThat(actualList, containsInAnyOrder(matchers)); }
进阶优化:用反射简化属性对比
如果两个类的属性非常多,逐一写对比代码太麻烦,可以用Apache Commons的EqualsBuilder工具类通过反射对比属性:
首先引入依赖:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> <scope>test</scope> </dependency>
然后修改自定义匹配器的matchesSafely方法:
@Override protected boolean matchesSafely(GsonSomething actual) { // 反射对比所有非transient属性(属性名和类型需一致) return EqualsBuilder.reflectionEquals(expected, actual, false); }
这样不管有多少属性,都能一次性对比完成,非常方便!
内容的提问来源于stack exchange,提问作者S.Dan

