在没有equals方法的情况下,如何在两个类上声明相等性?


111

说我有一个没有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()不会告诉我哪些字段相等失败。

有没有更好的实践来从对象中获取我想要的东西,而又不继承和覆盖等于(ugh)?


您是否正在寻找可以为您做深度比较的图书馆?就像在stackoverflow.com/questions/1449001/…中建议的深等于?
维克多

3
为什么您需要知道为什么两个实例不相等。通常,equal方法的实现仅告诉两个实例是否相等,我们不在乎为什么实例不相等。
Bhesh Gurung

3
我想知道哪些属性不相等,所以我可以修复它们。:)
瑞安·纳尔逊

所有Object都有一个equals方法,您可能意味着没有覆盖的equals方法。
史蒂夫·郭

我能想到的最好方法是使用包装器类或子类,然后在覆盖equals方法之后使用它
。.– Thihara

Answers:


66

Mockito提供了一个反射匹配器:

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

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

对于旧版本,请使用:

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

17
此类在package中org.mockito.internal.matchers.apachecommons。Mockito文档指出:org.mockito.internal->“内部类,不被客户端使用。” 使用此工具会使您的项目面临风险。这可以在任何Mockito版本中更改。在这里阅读:site.mockito.org/mockito/docs/current/overview-summary.html
luboskrnac


1
Mockito.refEq()当对象没有ID集=(
cavpollo

1
@PiotrAleksanderChmielowski,很抱歉,当使用​​Spring + JPA + Entities时,Entity对象可能具有一个id(代表数据库表的id字段),因此当它为空(一个尚未存储在数据库中的新对象)时,它将refEq失败比较,因为hashcode方法无法比较对象。
cavpollo

3
它工作正常,但预期和实际的顺序错误。相反。
pkawiak

48

这里有许多正确答案,但我也想添加我的版本。这基于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中,该方法已过时,伍德兹在评论中指出。目前的建议是

public class TestClass {

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

}

3
在assertj v3.13.2此方法已被弃用,现在建议是使用usingRecursiveComparison()isEqualTo(),例如该线是assertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
Woodz

45

我通常使用org.apache.commons.lang3.builder.EqualsBuilder实现此用例

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

1
摇篮:androidTestCompile'org.apache.commons:commons-lang3:3.5'–
Roel

1
当您要使用“ org.apache.commons.lang3.builder.EqualsBuilder”时,需要将其添加到“依赖关系”下的gradle文件中
Roel

3
这没有给出任何确切字段实际不匹配的提示。
Vadzim

1
@Vadzim我已经使用下面的代码来获取Assert.assertEquals(ReflectionToStringBuilder.toString(expected),ReflectionToStringBuilder.toString(actual));
Abhijeet Kushe

2
这要求图中的所有节点都实现“等于”和“哈希码”,这基本上使该方法几乎无用。AssertJ的isEqualToComparingFieldByFieldRecursively是在我的情况下完美工作的那个。
张翰

12

我知道它有些旧,但是希望对您有所帮助。

我遇到了与您相同的问题,因此,经过调查,我发现的问题几乎与此类似,并且在找到解决方案后,我也在那些问题中回答相同的问题,因为我认为这可以帮助他人。

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

基本上,它包括使用称为Unitils的库。

这是用途:

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

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


1
不幸的是Unitils似乎已经死亡,请参阅stackoverflow.com/questions/34658067/is-unitils-project-alive
肯·威廉姆斯

8

您可以使用Apache Commons lang ReflectionToStringBuilder

您可以指定要逐一测试的属性,或者更好地排除不需要的属性:

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

然后,您可以正常比较两个字符串。对于反射缓慢的问题,我认为这仅用于测试,因此不应该那么重要。


1
这种方法的另一个好处是,您可以获得视觉输出,以期望值和实际值的形式显示为字符串,排除了您不需要的字段。
fquinner

7

如果您在断言(assertThat)中使用hamcrest,并且不想引入其他测试库,则可以SamePropertyValuesAs.samePropertyValuesAs断言没有重写equals方法的项目。

好处是,您不必引入另一个测试框架,并且当assert失败(expected: field=<value> but was field=<something else>)时(而不是expected: true but was false使用类似的东西),它将给出一个有用的错误EqualsBuilder.reflectionEquals()

缺点是比较浅,没有排除字段的选项(例如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,您必须添加projectDependecyhamcrest.org/JavaHamcrest/…–
bigspawn

我喜欢这个解决方案,因为它没有添加“完全*不同的其他依赖项!”
GhostCat 19'Aug


2

一些反射比较方法比较浅

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


1

逐场比较:

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);

1
这是我不希望做的事情,因为早期的断言失败将在下面隐藏可能的失败。
瑞安·纳尔逊

2
抱歉,我错过了您的那部分帖子……为什么“完全平等的画面”在单元测试环境中很重要?这些字段要么全部相等(测试通过),要么不全部相等(测试失败)。
EthanB

我不想重新运行测试以发现其他字段是否相等。我想预先知道所有不相等的字段,因此我可以立即解决它们。
瑞安·尼尔森

1
在一个测试中声明许多字段将不被视为真正的“单元”测试。使用传统的测试驱动开发(TDD),您可以编写一个小的测试,然后编写足够的代码以使其通过。每个字段只有一个断言是正确的方法,只是不要将所有断言放在一个测试中。为您关心的每个字段声明创建一个不同的测试。只需运行一次套件,您就可以查看所有字段的所有错误。如果这很困难,则可能意味着您的代码最初没有足够的模块化,并且很可能可以重构为更干净的解决方案。
杰西·韦伯

这绝对是一个有效的建议,并且是您在单个测试中解决多个断言的通常方法。这里唯一的挑战是,我想从整体上观察物体。也就是说,我想同时测试所有字段以验证对象处于有效状态。当您具有覆盖的equals()方法时(在本示例中我当然没有),这并不难做到。
瑞安·尼尔森

1

AssertJ断言可用于比较值,而#equals方法未正确覆盖,例如:

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

// ...

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

1

由于这个问题很旧,我将建议使用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()));

0

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


0

这是一个通用的compare方法,该方法将同一个类的两个对象的it字段值进行比较(请注意,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 ;
    }
}

0

我偶然发现了一个非常相似的案例。

我希望能在一个测试比较,一个对象有相同的属性值作为另外一个,但像法is()refEq()等不会像在其空值我的对象的原因,工作id属性。

所以这是我找到的解决方案(嗯,找到了一个同事):

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,则它们在某些属性上有所不同。



0

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

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

assertEquals(expected, actual);

与手动比较每个字段相比,此方法的优势在于您可以比较所有字段,因此,即使您以后在类中添加新字段,与使用一堆assertEquals()on 字段相比,它也将得到自动测试。所有字段,如果您将更多字段添加到班级中,则需要对其进行更新。

jUnit还会为您显示字符串,以便您可以直接查看它们的不同之处。但是不确定字段排序的可靠性如何Gson,这可能是一个潜在的问题。


1
Gson不保证字段顺序。您可能想JsonParse字符串并比较解析产生的JsonElements
ozma

0

我尝试了所有答案,但实际上没有任何效果。

因此,我创建了自己的方法,该方法可以比较简单的java对象,而无需深入嵌套结构...

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

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

如何使用

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

如果不匹配,则输出样本

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

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

代码(Java 8及更高版本):

 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);
    }

0

作为纯junit的替代方法,可以在等于声明之前将字段设置为null:

    actual.setCreatedDate(null); // excludes date assertion
    expected.setCreatedDate(null);

    assertEquals(expected, actual);

-1

您可以将发布的比较代码放入某种静态实用程序方法中吗?

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(“对象不相等”,“”,findDifferences(obj1,obj));

这并不笨拙,并且会为您提供有关差异(如果存在)的完整信息(通过不完全以assertEqual的正常形式出现,但是您会获得所有信息,因此应该很好)。


-1

这不会对OP有所帮助,但可能会帮助到这里结束的所有C#开发人员...

就像张贴的Enrique一样,您应该重写equals方法。

有没有更好的实践来从对象中获取我想要的东西,而又不继承和覆盖等于(ugh)?

我的建议是不要使用子类。使用局部类。

部分类定义(MSDN)

所以您的课程看起来像...

public partial class TheClass
{
    public override bool Equals(Object obj)
    {
        // your implementation here
    }
}

对于Java,我同意使用反射的建议。请记住,您应该尽可能避免使用反射。它很慢,很难调试,甚至很难维护,因为IDE可能会通过重命名字段或类似的方式来破坏您的代码。小心!


-1

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

仅出于讨论目的,可以说该类确实覆盖了equals方法。

因此,您的UT如下所示:

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

Assert.assertEquals(expected, actual). 

您完成了。而且,如果断言失败,您将无法获得“完全平等的画面”。

据我了解,您是在说,即使类型确实覆盖了均等值,您也不会对它感兴趣,因为您希望获得“完全均等图”。因此,扩展和覆盖相等也没有意义。

因此,您必须选择:使用反射对每个属性进行比较,或者使用反射或硬编码检查,我建议使用后者。或者:比较这些对象的人类可读表示

例如,您可以创建一个帮助程序类,该类将希望比较的类型序列化为XML文档,然后比较生成的XML!在这种情况下,您可以直观地看到什么完全相等和什么不相等。

这种方法将使您有机会查看整个图片,但它也相对麻烦(起初容易出错)。


我的“完全平等图片”一词可能令人困惑。实现equals()确实可以解决问题。我有兴趣同时知道所有不相等的字段(与相等有关),而不必重新运行测试。序列化对象是另一种可能性,但我不一定需要很深的平等。如果可能,我想利用属性的equals()实现。
瑞安·尼尔森

大!正如您在问题中所述,您绝对可以利用属性的均等值。在这种情况下,这似乎是最直接的解决方案,但是正如您所指出的,代码可能非常讨厌。
Vitaliy 2012年

-3

您可以像这样覆盖类的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和哈希码方法


3
但OP表示他不想超越平等,他想要一个更好的方法
Ankur 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.