概述
有时候,我们无法覆盖一个类的equals()
方法。然而,我们仍然希望比较两个对象是否相同。本教程将介绍几种不使用equals()
方法来测试对象相等的方法。
示例类
在深入之前,我们先创建一些示例类。我们将使用Person
和Address
类:
public class Person {
private Long id;
private String firstName;
private String lastName;
private Address address;
// getters and setters
}
public class Address {
private Long id;
private String city;
private String street;
private String country;
// getters and setters
}
这两个类都没有重写equals()
方法,因此在判断相等性时,会使用Object
类提供的默认实现。换句话说,Java在检查相等性时会检查两个引用是否指向同一个对象。
使用AssertJ
AssertJ库提供了使用递归比较来比较对象的功能。它通过内省确定应比较哪些字段和值。
首先,在pom.xml
中添加assertj-core依赖:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.21.0</version>
<scope>test</scope>
</dependency>
要检查两个Person
实例的字段是否包含相同的值,我们可以在调用isEqualTo()
方法之前使用usingRecursiveComparison()
方法:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(1L, "Jane", "Doe");
assertThat(actual).usingRecursiveComparison().isEqualTo(expected);
算法首先获取实际对象的字段,然后与预期对象的相应字段进行比较。但是,比较并不对称。预期对象可以比实际对象具有更多的字段。
此外,我们可以使用ignoringFields()
方法忽略特定字段:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(2L, "Jane", "Doe");
assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("id")
.isEqualTo(expected);
对于复杂的对象,这种方法也十分有效:
Person expected = new Person(1L, "Jane", "Doe");
Address address1 = new Address(1L, "New York", "Sesame Street", "United States");
expected.setAddress(address1);
Person actual = new Person(1L, "Jane", "Doe");
Address address2 = new Address(1L, "New York", "Sesame Street", "United States");
actual.setAddress(address2);
assertThat(actual)
.usingRecursiveComparison()
.isEqualTo(expected);
使用Hamcrest
Hamcrest库利用反射检查两个对象是否具有相同的属性,并创建一个匹配器来检查实际对象是否具有与预期对象相同的值。
首先,在pom.xml
中添加Hamcrest依赖:
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
现在,我们可以调用samePropertyValuesAs()
方法并传入预期对象:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(1L, "Jane", "Doe");
MatcherAssert.assertThat(actual, samePropertyValuesAs(expected));
与前一个示例类似,我们可以指定要忽略的字段名。这些字段将从预期和实际对象中移除。
然而,背后的操作是Hamcrest使用反射从某些字段获取值。在进行相等性检查时,每个字段都会调用equals()
方法。
这意味着,如果我们的Address
类也没有重写equals()
方法,上述代码在处理复杂对象时将不会工作,因为会检查内存中两个Address
引用是否指向同一对象。因此,断言会失败。
如果我们想比较复杂对象,我们需要分别比较它们:
Person expected = new Person(1L, "Jane", "Doe");
Address address1 = new Address(1L, "New York", "Sesame Street", "United States");
expected.setAddress(address1);
Person actual = new Person(1L, "Jane", "Doe");
Address address2 = new Address(1L, "New York", "Sesame Street", "United States");
actual.setAddress(address2);
MatcherAssert.assertThat(actual, samePropertyValuesAs(expected, "address"));
MatcherAssert.assertThat(actual.getAddress(), samePropertyValuesAs(expected.getAddress()));
这里,我们在第一个断言中排除了address
字段,然后在第二个断言中单独比较。
使用Apache Commons Lang3
现在,让我们看看如何使用Apache Commons库进行相等性检查。
在pom.xml
中添加Apache Commons Lang3依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
<scope>test</scope>
</dependency>
5.1. ReflectionToStringBuilder
类
Apache Commons库提供了一个名为ReflectionToStringBuilder
的类。它允许我们通过反射对象的字段和值生成对象的字符串表示形式。
通过比较两个对象的字符串表示形式,我们可以断言它们的相等性,而无需使用equals()
方法:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(1L, "Jane", "Doe");
assertThat(ReflectionToStringBuilder.toString(actual, ToStringStyle.SHORT_PREFIX_STYLE))
.isEqualTo(ReflectionToStringBuilder.toString(expected, ToStringStyle.SHORT_PREFIX_STYLE));
然而,我们仍然需要在类中重写toString()
方法。
5.2. EqualsBuilder
类
另一种选择是使用EqualsBuilder
类:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(1L, "Jane", "Doe");
assertTrue(EqualsBuilder.reflectionEquals(expected, actual));
它使用Java反射API来比较两个对象的字段。值得注意的是,reflectionEquals()
方法执行的是浅层相等检查。
因此,当比较复杂对象时,我们需要忽略这些字段并分别比较它们:
Person expected = new Person(1L, "Jane", "Doe");
Address address1 = new Address(1L, "New York", "Sesame Street", "United States");
expected.setAddress(address1);
Person actual = new Person(1L, "Jane", "Doe");
Address address2 = new Address(1L, "New York", "Sesame Street", "United States");
actual.setAddress(address2);
assertTrue(EqualsBuilder.reflectionEquals(expected, actual, "address"));
assertTrue(EqualsBuilder.reflectionEquals(expected.getAddress(), actual.getAddress()));
使用Mockito
另一种比较实例相等的方式是使用Mokito。
我们需要添加mockito-core依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
现在,我们可以使用Mockito的ReflectionEquals
类:
Person expected = new Person(1L, "Jane", "Doe");
Person actual = new Person(1L, "Jane", "Doe");
assertTrue(new ReflectionEquals(expected).matches(actual));
此外,当进行相等性检查时,会调用Apache Commons库中的EqualsBuilder
。
再次,对于复杂对象的比较,我们需要使用与EqualsBuilder
相同的策略:
Person expected = new Person(1L, "Jane", "Doe");
Address address1 = new Address(1L, "New York", "Sesame Street", "United States");
expected.setAddress(address1);
Person actual = new Person(1L, "Jane", "Doe");
Address address2 = new Address(1L, "New York", "Sesame Street", "United States");
actual.setAddress(address2);
assertTrue(new ReflectionEquals(expected, "address").matches(actual));
assertTrue(new ReflectionEquals(expected.getAddress()).matches(actual.getAddress()));
总结
在这篇文章中,我们学习了如何在不使用equals()
方法的情况下断言两个实例的相等性。
总的来说,AssertJ的字段级比较为比较复杂对象提供了最简单的方法,而其他方法则使用反射来比较字段,所以我们需要为复杂的字段添加额外的断言。
通过利用本文中提到的工具,即使面对没有equals()
方法的对象,我们也能编写测试。
如往常一样,完整的源代码可在GitHub上找到。