ChatGPT解决这个技术问题 Extra ChatGPT

如何在没有 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);

这给了我完整的平等画面,但很笨拙(我什至没有考虑可能的空问题)。第三种选择是使用 Comparator,但 compareTo() 不会告诉我哪些字段不相等。

有没有更好的做法来从对象中获取我想要的东西,而不需要子类化和覆盖等于(呃)?

您是否正在寻找可以为您进行深度比较的库?像 stackoverflow.com/questions/1449001/… 建议的 deep-equals 吗?
为什么你需要知道为什么两个实例不相等。通常,equal 方法的实现只告诉两个实例是否相等,我们并不关心为什么实例不相等。
我想知道哪些属性不相等,以便修复它们。 :)
所有 Object 都有一个 equals 方法,您可能意味着没有覆盖的 equals 方法。
我能想到的最好方法是使用包装类或子类,然后在覆盖 equals 方法后使用它。

T
Thomas

这里有很多正确的答案,但我也想添加我的版本。这是基于 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 v3.13.2 中,此方法已弃用,现在建议将 usingRecursiveComparison()isEqualTo() 一起使用,这样该行是 assertThat(actualObject).usingRecursiveComparison().isEqualTo(expectedObject);
f
felvhage

Mockito 提供反射匹配器:

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

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

对于旧版本,请使用:

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

此类在包 org.mockito.internal.matchers.apachecommons 中。 Mockito 文档状态:org.mockito.internal -> “内部类,不供客户使用。”使用它,您将把您的项目置于风险之中。这可以在任何 Mockito 版本中更改。在这里阅读:site.mockito.org/mockito/docs/current/overview-summary.html
请改用 Mockito.refEq()
当对象没有设置 ID 时,Mockito.refEq() 失败 =(
@PiotrAleksanderChmielowski,抱歉,在使用 Spring+JPA+Entities 时,Entity 对象可能有一个 id(代表数据库表的 id 字段),所以当它为空时(一个尚未存储在 DB 上的新对象),{ 1} 无法比较,因为 hashcode 方法无法比较对象。
它工作正常,但预期和实际的顺序错误。应该反过来。
A
Abhijeet Kushe

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

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

摇篮:androidTestCompile 'org.apache.commons:commons-lang3:3.5'
当您想使用“org.apache.commons.lang3.builder.EqualsBuilder”时,您需要将其添加到“依赖项”下的 gradle 文件中
这并没有暗示哪些确切的字段实际上不匹配。
@Vadzim 我已经使用下面的代码来获取 Assert.assertEquals(ReflectionToStringBuilder.toString(expected), ReflectionToStringBuilder.toString(actual));
这要求图中的所有节点都实现“相等”和“哈希码”,这基本上使这种方法几乎没用。 AssertJ 的 isEqualToComparingFieldByFieldRecursively 是在我的情况下完美运行的方法。
C
Community

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

我遇到了和你一样的问题,所以,经过调查,我发现很少有与这个问题相似的问题,并且在找到解决方案后,我在这些问题中回答相同,因为我认为它可以帮助别人。

这个similar questionThe most voted answer(不是作者选择的)是最适合您的解决方案。

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

这是用途:

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

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


不幸的是,Unitils 似乎已经死了,请参阅 stackoverflow.com/questions/34658067/is-unitils-project-alive
M
Marquee

如果您将 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)。


对于工作 SamePropertyValuesAs,您必须添加项目依赖hamcrest.org/JavaHamcrest/…
我喜欢这个解决方案,因为它没有添加“完全*不同的附加依赖项!
另一个缺点:它使用属性(getter)而不是比较字段。
M
Matthew Farwell

您可以使用 Apache commons lang ReflectionToStringBuilder

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

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

然后,您可以正常比较这两个字符串。关于反射缓慢的观点,我认为这只是为了测试,所以不应该那么重要。


这种方法的另一个好处是,您可以获得将预期值和实际值显示为字符串的视觉输出,不包括您不关心的字段。
有没有办法排除那里存在的转换为字符串类型的地址值
p
patelb

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

S
Stefan Birkner

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

assertThat(obj1, reflectEquals(obj2));

A
Avraham Shalev

一些反射比较方法很浅

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

L
Leonel Sanches da Silva

使用 Shazamcrest,您可以:

assertThat(obj1, sameBeanAs(obj2));

为什么使用 shazamcrest 而不是 hamcrest ? stackoverflow.com/a/27817702/6648326
@MasterJoe2 在我的测试中,当对象具有不同的引用时,reflectEquals 返回 false
P
Pavel

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

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

// ...

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

E
EthanB

逐个字段比较:

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

这是我不想做的事情,因为早期的断言失败会隐藏下面可能的失败。
抱歉,我错过了您帖子的那一部分……为什么“完全平等的图片”在单元测试环境中很重要?字段要么全部相等(测试通过),要么它们不全部相等(测试失败)。
我不想重新运行测试来发现其他字段是否不相等。我想预先知道所有不相等的字段,以便我可以立即解决它们。
在一个测试中断言多个字段不会被视为真正的“单元”测试。使用传统的测试驱动开发 (TDD),您只需编写一个小测试,然后只需编写足够的代码即可使其通过。每个字段有一个断言是正确的方法,只是不要将所有断言放入一个测试中。为您关心的每个字段断言创建不同的测试。这将允许您通过一次运行套件查看所有字段的所有错误。如果这很难,则可能意味着您的代码一开始就不够模块化,并且可能会被重构为更清洁的解决方案。
这绝对是一个有效的建议,也是您在单个测试中解决多个断言的常用方法。这里唯一的挑战是,我想要一个对象的整体视图。也就是说,我想同时测试所有字段以验证对象是否处于有效状态。当你有一个重写的 equals() 方法时,这并不难做到(当然我在这个例子中没有)。
j
jtahlborn

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


关于基本原理的最佳方法,但暗示一些像 Apache Commons 这样的可读性会更好。尽管如此,我还是投了赞成票。
A
Andrew Taran

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

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

S
Shantonu

这是一个通用的比较方法,它比较同一类的两个对象的字段值(记住那些可以通过 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 ;
    }
}

c
cavpollo

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

我想在测试中比较一个对象与另一个对象具有相同的属性值,但像 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,则它们在某些属性上有所不同。


G
Grigory Kislin

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

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

Using a custom comparison strategy in assertions

AssertJ examples


M
Magnus

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

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

assertEquals(expected, actual);

与逐个字段手动比较相比,这样做的优势在于您可以比较所有您的字段,因此即使您稍后向您的类添加一个新字段,它也会被自动测试,与 if 相比您在所有字段上使用了一堆 assertEquals(),如果您向班级添加更多字段,则需要对其进行更新。

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


Gson 不保证字段顺序。您可能想要 JsonParse 字符串并比较解析结果的 JsonElements
S
Stan Sokolov

我尝试了所有答案,但没有什么对我真正有用。

所以我创建了自己的方法来比较简单的 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);
    }

J
Jacob Nordfalk

对于单元测试,我只是将对象序列化为 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(但如果有,您可能会考虑为什么对相同的数据有不同的类......以及它们最终如何在测试方法中进行比较 :-)


K
Kamil

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

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 的正常形式,但您可以获得所有信息,所以它应该是好的)。


V
Vitaliy

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

只是为了讨论,假设该类确实覆盖了 equals 方法。

所以你的 UT 看起来像:

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

Assert.assertEquals(expected, actual). 

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

据我了解,您是说即使类型确实覆盖了equals,您也不会对此感兴趣,因为您想获得“完全相等的图片”。因此,扩展和覆盖 equals 也没有任何意义。

所以你必须选择:要么逐个比较属性,使用反射或硬编码检查,我建议后者。或者:比较这些对象的人类可读表示。

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

这种方法将使您有机会查看全貌,但它也相对繁琐(起初容易出错)。


我的术语“完全平等图景”可能令人困惑。实现 equals() 确实可以解决问题。我有兴趣同时了解所有不相等的字段(与相等相关),而不必重新运行测试。序列化对象是另一种可能性,但我不一定需要深度相等。如果可能,我想利用属性的 equals() 实现。
伟大的!正如您在问题中所述,您绝对可以使用相等的属性。在这种情况下,这似乎是最直接的解决方案,但正如您所指出的,代码可能非常讨厌。
E
Enrique San Martín

您可以覆盖类的 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 code 方法


但是OP说他不想覆盖equals,他想要一个更好的方法