如何在没有 equals 方法的两个类上断言相等?

假设我有一个没有 equals ()方法的类,它没有源。我想对该类的两个实例断言相等。

我可以做多重断言:

assertEquals(obj1.getFieldA(), obj2.getFieldA());
assertEquals(obj1.getFieldB(), obj2.getFieldB());
assertEquals(obj1.getFieldC(), obj2.getFieldC());
...

我不喜欢这个解决方案,因为如果早期的断言失败了,我就无法得到完整的等式图像。

我可以自己手动比较并跟踪结果:

String errorStr = "";
if(!obj1.getFieldA().equals(obj2.getFieldA())) {
errorStr += "expected: " + obj1.getFieldA() + ", actual: " + obj2.getFieldA() + "\n";
}
if(!obj1.getFieldB().equals(obj2.getFieldB())) {
errorStr += "expected: " + obj1.getFieldB() + ", actual: " + obj2.getFieldB() + "\n";
}
...
assertEquals("", errorStr);

这给了我一个完整的等式图片,但是很笨重(我甚至没有考虑可能的空问题)。第三个选项是使用 Compaator,但 compareTo ()不会告诉我哪些字段的相等性失败。

有没有一种更好的做法来从对象中得到我想要的东西,而不需要子类化和重写 equals (ugh) ?

179276 次浏览

可以重写类的 equals 方法,如:

@Override
public int hashCode() {
int hash = 0;
hash += (app != null ? app.hashCode() : 0);
return hash;
}


@Override
public boolean equals(Object object) {
HubRule other = (HubRule) object;


if (this.app.equals(other.app)) {
boolean operatorHubList = false;


if (other.operator != null ? this.operator != null ? this.operator
.equals(other.operator) : false : true) {
operatorHubList = true;
}


if (operatorHubList) {
return true;
} else {
return false;
}
} else {
return false;
}
}

好吧,如果你想比较一个类中的两个对象,你必须以某种方式实现 equals 和 hash 代码方法

逐个领域比较:

assertNotNull("Object 1 is null", obj1);
assertNotNull("Object 2 is null", obj2);
assertEquals("Field A differs", obj1.getFieldA(), obj2.getFieldA());
assertEquals("Field B differs", obj1.getFieldB(), obj2.getFieldB());
...
assertEquals("Objects are not equal.", obj1, obj2);

你能把你发布的比较代码放到一些静态实用方法中吗?

public static String findDifference(Type obj1, Type obj2) {
String difference = "";
if (obj1.getFieldA() == null && obj2.getFieldA() != null
|| !obj1.getFieldA().equals(obj2.getFieldA())) {
difference += "Difference at field A:" + "obj1 - "
+ obj1.getFieldA() + ", obj2 - " + obj2.getFieldA();
}
if (obj1.getFieldB() == null && obj2.getFieldB() != null
|| !obj1.getFieldB().equals(obj2.getFieldB())) {
difference += "Difference at field B:" + "obj1 - "
+ obj1.getFieldB() + ", obj2 - " + obj2.getFieldB();
// (...)
}
return difference;
}

然后您可以像下面这样在 JUnit 中使用这个方法:

AssertEquals (“对象不相等”,“”,finddifferent (objec1,obj)) ;

它并不笨重,如果存在差异,它会提供给你关于差异的全部信息(虽然不是正常形式的 assertequals,但是你得到了所有的信息,所以它应该是好的)。

您可以使用反射来“自动化”完全相等性测试。您可以实现为单个字段编写的相等“跟踪”代码,然后使用反射在对象中的所有字段上运行该测试。

从你的评论到其他的回答,我不明白你想要什么。

为了便于讨论,假设该类确实重写了 equals 方法。

所以你的 UT 看起来像这样:

SomeType expected = // bla
SomeType actual = // bli


Assert.assertEquals(expected, actual).

而且,如果断言失败,就不能得到“完全相等的图像”。

据我所知,您的意思是即使该类型确实覆盖了 equals,您也不会对它感兴趣,因为您希望得到“完全相等的图片”。因此,扩展和重写 equals 也没有意义。

因此,您必须做出选择: 要么按属性比较属性,要么使用反射或硬编码检查,我建议使用后者。或者: 比较这些对象的人类可读 申述

例如,您可以创建一个 helper 类,该类序列化您希望与 XML 文档比较的类型,然后比较结果 XML!在这种情况下,您可以直观地看到什么是相等的,什么是不相等的。

这种方法将使您有机会了解整个情况,但是它也相对比较麻烦(并且一开始有一点错误倾向)。

你可以使用 Apache commons lang AffectionToStringBuilder

您可以一个一个地指定要测试的属性,或者更好的方法是排除那些您不想测试的属性:

String s = new ReflectionToStringBuilder(o, ToStringStyle.SHORT_PREFIX_STYLE)
.setExcludeFieldNames(new String[] { "foo", "bar" }).toString()

然后将这两个字符串作为正常值进行比较。关于反射缓慢的问题,我假设这只是用于测试,因此不应该如此重要。

我通常使用 org.apache.commans.lang3.builder 来实现这个用例

Assert.assertTrue(EqualsBuilder.reflectionEquals(expected,actual));

我知道有点旧了,但我希望能有所帮助。

我遇到了和你一样的问题,所以,经过调查,我发现几乎没有类似的问题,比这个问题,并且,在找到解决方案之后,我正在回答同样的问题,因为我认为它可以帮助其他人。

这个 类似的问题中投票最多的答案 (不是作者选择的答案) ,是最适合你的解决方案。

基本上,它包括使用名为 直到的库。

这就是它的用途:

User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);

即使类 User没有实现 equals(),它也会通过。您可以在它们的 教程中看到更多的示例和一个非常酷的名为 assertLenientEquals的断言。

Hamcrest 1.3通用匹配器有一个特殊的匹配器,它使用反射而不是等于。

assertThat(obj1, reflectEquals(obj2));

这是一个通用的比较方法,它比较同一个类的两个对象及其字段的值(请记住这些对象是可以通过 get 方法访问的)

public static <T> void compare(T a, T b) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
AssertionError error = null;
Class A = a.getClass();
Class B = a.getClass();
for (Method mA : A.getDeclaredMethods()) {
if (mA.getName().startsWith("get")) {
Method mB = B.getMethod(mA.getName(),null );
try {
Assert.assertEquals("Not Matched = ",mA.invoke(a),mB.invoke(b));
}catch (AssertionError e){
if(error==null){
error = new AssertionError(e);
}
else {
error.addSuppressed(e);
}
}
}
}
if(error!=null){
throw error ;
}
}

我无意中发现了一个非常相似的案子。

我想在一个测试中比较一个对象的属性值是否与另一个对象相同,但是像 is()refEq()等方法不能工作,原因是我的对象的 id属性中有一个 null 值。

这就是我找到的解决方案(一个同事找到的) :

import static org.apache.commons.lang.builder.CompareToBuilder.reflectionCompare;


assertThat(reflectionCompare(expectedObject, actualObject, new String[]{"fields","to","be","excluded"}), is(0));

如果从 reflectionCompare获得的值为0,则表示它们相等。如果是 -1或1,它们在某些属性上有所不同。

这里有很多正确的答案,但是我想加上我的版本,这是基于 Assertj 的。

import static org.assertj.core.api.Assertions.assertThat;


public class TestClass {


public void test() {
// do the actual test
assertThat(actualObject)
.isEqualToComparingFieldByFieldRecursively(expectedObject);
}
}

更新: 在 assertj v3.13.2中,正如 Woodz 在评论中指出的那样,这个方法已被废弃

public class TestClass {


public void test() {
// do the actual test
assertThat(actualObject)
.usingRecursiveComparison()
.isEqualTo(expectedObject);
}


}

在 AssertJ 的常见情况下,您可以创建自定义比较器策略:

assertThat(frodo).usingComparator(raceComparator).isEqualTo(sam)
assertThat(fellowshipOfTheRing).usingElementComparator(raceComparator).contains(sauron);

在断言中使用自定义比较策略

AssertJ 示例

如果您使用 hamcrest 作为断言(assertThat) ,并且不想引入额外的测试库,那么可以使用 SamePropertyValuesAs.samePropertyValuesAs来断言没有重写的 equals 方法的项。

好的一面是,您不必再使用另一个测试框架,如果您使用类似于 EqualsBuilder.reflectionEquals()的东西,当断言失败(expected: field=<value> but was field=<something else>)而不是 expected: true but was false时,它会给出一个有用的错误。

缺点是它是一个浅表的比较,并且没有排除字段的选项(就像 EqualsBuilder 中一样) ,所以您必须处理嵌套对象(例如,删除它们并单独比较它们)。

最佳案例:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
assertThat(actual, is(samePropertyValuesAs(expected)));

丑陋案例:

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
...
SomeClass expected = buildExpected();
SomeClass actual = sut.doSomething();


assertThat(actual.getSubObject(), is(samePropertyValuesAs(expected.getSubObject())));
expected.setSubObject(null);
actual.setSubObject(null);


assertThat(actual, is(samePropertyValuesAs(expected)));

因此,选择你的毒药。额外的框架(例如 Unitils) ,无用的错误(例如 EqualsBuilder) ,或者浅层比较(hamcrest)。

一些反射比较方法比较浅显

另一种选择是将对象转换为 json 并比较字符串。

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public static String getJsonString(Object obj) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return bjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (JsonProcessingException e) {
LOGGER.error("Error parsing log entry", e);
return null;
}
}
...
assertEquals(getJsonString(MyexpectedObject), getJsonString(MyActualObject))

在单元测试 Android 应用程序时,我遇到了完全相同的难题,我想到的最简单的解决方案是使用 葛森将我的实际值和期望值对象转换成 json,并将它们作为字符串进行比较。

String actual = new Gson().toJson( myObj.getValues() );
String expected = new Gson().toJson( new MyValues(true,1) );


assertEquals(expected, actual);

相对于手动逐个字段比较的优势在于,你可以比较 所有你的字段,所以即使你稍后在类中添加一个新字段,它也会自动测试,相比之下,如果你在所有字段中使用一堆 assertEquals(),那么如果你在类中添加更多的字段,它就需要更新。

JUnit 还为您显示字符串,这样您可以直接看到它们的不同之处。虽然不确定 Gson的字段排序有多可靠,但这可能是一个潜在的问题。

使用 Shazamcrest,你可以:

assertThat(obj1, sameBeanAs(obj2));

我尝试了所有的答案,但没有一个真正适合我。

所以我创建了自己的方法来比较简单的 java 对象,而不用深入到嵌套结构中..。

如果所有字段匹配或包含不匹配详细信息的字符串匹配,则。

只比较具有 getter 方法的属性。

怎么用

        assertNull(TestUtils.diff(obj1,obj2,ignore_field1, ignore_field2));

如果有不匹配的样本输出

输出显示比较对象的属性名和各自的值

alert_id(1:2), city(Moscow:London)

代码(Java8及以上) :

 public static String diff(Object x1, Object x2, String ... ignored) throws Exception{
final StringBuilder response = new StringBuilder();
for (Method m:Arrays.stream(x1.getClass().getMethods()).filter(m->m.getName().startsWith("get")
&& m.getParameterCount()==0).collect(toList())){


final String field = m.getName().substring(3).toLowerCase();
if (Arrays.stream(ignored).map(x->x.toLowerCase()).noneMatch(ignoredField->ignoredField.equals(field))){
Object v1 = m.invoke(x1);
Object v2 = m.invoke(x2);
if ( (v1!=null && !v1.equals(v2)) || (v2!=null && !v2.equals(v1))){
response.append(field).append("(").append(v1).append(":").append(v2).append(")").append(", ");
}
}
}
return response.length()==0?null:response.substring(0,response.length()-2);
}

AssertJ 断言可以用来比较没有正确覆盖 #equals方法的值,例如:

import static org.assertj.core.api.Assertions.assertThat;


// ...


assertThat(actual)
.usingRecursiveComparison()
.isEqualTo(expected);

由于这个问题已经很老了,我将推荐另一种使用 JUnit5的现代方法。

我不喜欢这个解决方案,因为如果早期的断言失败了,我就无法得到完整的等式图像。

对于 JUnit 5,有一个称为 Assertions.assertAll()的方法,它允许您将测试中的所有断言分组在一起,它将执行每一个断言并在最后输出任何失败的断言。这意味着任何首先失败的断言都不会停止后一个断言的执行。

assertAll("Test obj1 with obj2 equality",
() -> assertEquals(obj1.getFieldA(), obj2.getFieldA()),
() -> assertEquals(obj1.getFieldB(), obj2.getFieldB()),
() -> assertEquals(obj1.getFieldC(), obj2.getFieldC()));

对于 单元测试,我只是将对象序列化为一个 JSON 字符串并比较它。 例如 Gson:

import com.google.gson.GsonBuilder
import junit.framework.TestCase.assertEquals


class AssertEqualContent {
companion object {
val gson = GsonBuilder().create()


fun assertEqualContent(message: String?, expected: Any?, actual: Any?) {
assertEquals(message, gson.toJson(expected), gson.toJson(actual))
}
}
}

由于期望的对象和实际的对象应该是同一类型,因此字段顺序是相同的。

优点:

  • 您将得到一个很好的字符串比较,突出显示差异的确切位置。
  • 没有额外的库(假设您已经有一个 JSON 库)

缺点:

  • 不同类型的 也许吧对象生成相同的 JSON (但是如果它们生成相同的 JSON,您可能会考虑为什么对于相同的数据使用不同的类... ...)。以及它们最终如何在测试方法中进行比较: -)

如果您只是需要平面字段比较,可以使用 AssertJ

Assertions.assertThat(actual)).isEqualToComparingFieldByField(expected);