如何在没有equals方法的情况下对两个类进行相等性断言?

183

假设我有一个没有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);

这给了我完整的相等性图像,但很笨重(我甚至还没有考虑可能的空值问题)。第三个选项是使用Comparator,但compareTo()不会告诉我哪些字段未通过相等性检查。

有没有更好的做法来从对象中获取我想要的内容,而不用子类化和覆盖equals(呃)?


5
为什么需要知道这两个实例不相等的原因呢?通常,equal 方法的实现只会告诉我们这两个实例是否相等,我们并不关心它们为什么不相等。 - Bhesh Gurung
6
我想知道哪些属性不同,以便我可以修复它们。 :) - Ryan Nelson
1
所有的 Object 都有一个 equals 方法,你可能意味着没有重写 equals 方法。 - Steve Kuo
在我的情况下,该类没有覆盖 equals 方法,但是覆盖了 toString 方法,因此我正在使用它。 - Praveen Kumar
我认为这个问题涵盖了更多的细节,但由于您担心早期的断言可能会失败,这可能是值得关注的提示:'assertAll'始终检查传递给它的所有断言,无论有多少个失败。如果全部通过,则一切正常 - 如果至少有一个失败,则会得到所有错误(和正确)的详细结果。->请参见Nicolai Parlogs在https://dev59.com/zFkR5IYBdhLWcg3w0QRL上的答案 - Simeon
显示剩余2条评论
23个回答

128

这里有许多正确的答案,但我也想添加我的版本。这是基于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);
    }

}

14
在assertj v3.13.2中,这个方法已经被废弃了,现在的推荐做法是使用usingRecursiveComparison()isEqualTo()方法来代替。所以现在应该这样写:assertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject); - Woodz

89

Mockito提供反射匹配器:

对于Mockito的最新版本,请使用:

Assert.assertTrue(new ReflectionEquals(expected, excludeFields).matches(actual));

对于旧版本,请使用:

Assert.assertThat(actual, new ReflectionEquals(expected, excludeFields));

32
这个类在org.mockito.internal.matchers.apachecommons包中。Mockito文档说:org.mockito.internal -> "内部类,不应由客户端使用"。使用这个类可能会使您的项目面临风险。这可能会在任何Mockito版本中更改。请在此处阅读:http://site.mockito.org/mockito/docs/current/overview-summary.html。 - luboskrnac
10
请使用Mockito.refEq()代替。 - Jeremy Kao
2
当对象没有设置id时,Mockito.refEq()会失败 =( - cavpollo
1
@PiotrAleksanderChmielowski,抱歉,在使用Spring+JPA+Entities时,实体对象可能具有id(表示数据库表的id字段),因此当它为空(尚未存储在DB上的新对象)时,refEq无法进行比较,因为hashcode方法无法比较对象。 - cavpollo
3
功能正常,但期望值和实际值的顺序是错误的。应该颠倒过来。 - pkawiak
显示剩余4条评论

58

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

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

1
Gradle:androidTestCompile 'org.apache.commons:commons-lang3:3.5' - Roel
1
当您想要使用"org.apache.commons.lang3.builder.EqualsBuilder"时,您需要将此内容添加到gradle文件的"dependencies"下。 - Roel
8
这并没有提供任何关于哪些确切的字段实际上不匹配的提示。 - Vadzim
1
@Vadzim 我使用了以下代码来获取 Assert.assertEquals(ReflectionToStringBuilder.toString(expected), ReflectionToStringBuilder.toString(actual)); - Abhijeet Kushe
3
这个方法需要图中所有节点实现"equal"和"hashcode",这就使得此方法几乎无用。AssertJ的isEqualToComparingFieldByFieldRecursively在我的情况下完美地运作。 - John Zhang

15
我知道这有点老,但我希望它能有所帮助。 我遇到了与你相同的问题,所以在调查后,我找到了几个类似的问题,找到解决方案后,我在那些问题中回答了相同的问题,因为我认为它可以帮助其他人。 类似问题最受欢迎的答案(不是作者选择的答案)是最适合您的解决方案。 基本上,它包括使用名为Unitils的库。 这是用法:
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);

即使类 User 没有实现 equals(),这个方法也能通过。您可以在他们的tutorial中看到更多示例和一个非常棒的断言assertLenientEquals

4
很遗憾,“Unitils” 似乎已经不再维护,详情请见 https://dev59.com/vZLea4cB1Zd3GeqP6sdK 。 - Ken Williams

13

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

优点在于你不必引入另一个测试框架,而且如果断言失败,它将给出一个有用的错误提示(expected: field=<value> but was field=<something else>),而不是像使用EqualsBuilder.reflectionEquals()一样的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)。


1
对于工作中的 SamePropertyValuesAs,您必须在项目依赖项中添加 http://hamcrest.org/JavaHamcrest/distributables#adding-hamcrest-to-your-project。 - bigspawn
2
我喜欢这个解决方案,因为它不会添加“完全不同的额外依赖项”! - GhostCat
2
另一个缺点:它使用属性(getter)而非比较字段。 - Stanislav Bashkyrtsev

9

您可以使用Apache commons lang ReflectionToStringBuilder

您可以逐个指定要测试的属性,或者更好的方法是排除不需要的属性:

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

然后您可以像平常一样比较这两个字符串。至于反射过程速度较慢的问题,我认为这只是用于测试,所以不应该太重要。


1
这种方法的附加好处是,您可以获得可视化输出,显示预期和实际值作为字符串,排除您不关心的字段。 - fquinner
1
有没有一种方法可以在转换为字符串类型时排除存在的地址值? - Tushar Jajodia

9

由于这个问题比较旧了,我建议使用另一种现代方法来使用JUnit 5。

我不喜欢这个解决方案,因为如果早期断言失败,我就无法获取完整的相等性信息。

使用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()));

4

4

AssertJ断言可以用于比较没有正确重写#equals方法的值,例如:

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

// ...

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

3

一些反射比较方法是浅层比较。

另一个选项是将对象转换为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))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接