105

How to "deep"-compare two objects that do not implement the equals method based on their field values in a test?


Original Question (closed because lack of precision and thus not fulfilling SO standards), kept for documentation purposes:

I'm trying to write unit tests for a variety of clone() operations inside a large project and I'm wondering if there is an existing class somewhere that is capable of taking two objects of the same type, doing a deep comparison, and saying if they're identical or not?

fl0w
  • 3,593
  • 30
  • 34
Uri
  • 88,451
  • 51
  • 221
  • 321
  • 1
    How would this class know whether at a certain point of the object graphs it can accept identical objects, or only the same references? – Zed Sep 19 '09 at 17:20
  • Ideally it'll be configurable enough :) I'm looking for something automatic so that if new fields are added (and not cloned) the test can identify them. – Uri Sep 19 '09 at 17:24
  • 3
    What I'm trying to say is that you will need to configure (i.e. implement) the comparisons anyway. So then why not override the equals method in your classes and use that? – Zed Sep 19 '09 at 17:28
  • 3
    If equals returns false for a large complex object, where do you start? You are much better off turning the object into a multi-line String and doing a String comparison. Then you can see exactly where two object are different. IntelliJ pops up a "changes" comparison window which help find multiple changes between two results i.e. it understands the output of assertEquals(string1, string2) and gives you a comparison window. – Peter Lawrey Sep 30 '10 at 05:46
  • There are some really good answers here, besides the accepted one, that seem to have gotten buried – user1445967 Jan 18 '16 at 09:50
  • The answer to the question, is "Yes." Instead of using DeepEquals, there is another option. Use the GraphComparator.compare() method of java-util (https://github.com/jdereg/java-util) which will generate a difference list (List of Deltas) between the two graphs. If the list is empty, the graphs are equivalent. If the list has items in it, these items are the instructions to apply to the source to make it equal to the target graph. GraphComparator.applyDelta(source, deltaList) will bring the source up to match the target graph. It properly handles cycles within the graphs. – John DeRegnaucourt Mar 12 '16 at 20:29
  • Why not use Lombok's [EqualsAndHashCode annotation](https://projectlombok.org/features/EqualsAndHashCode)? – Suzana Nov 09 '22 at 10:26

14 Answers14

64

Unitils has this functionality:

Equality assertion through reflection, with different options like ignoring Java default/null values and ignoring order of collections

Wolfgang
  • 3,460
  • 3
  • 28
  • 38
  • 10
    I have done some testing on this function and it seems to do a deep comparison where as EqualsBuilder does not. – Howard May Mar 09 '12 at 15:19
  • Is there a way to have this not ignore transient fields? – Pinch Feb 13 '16 at 02:04
  • @Pinch I hear you. I would say the deep compare tool in `unitils` is flawed precisely because it compares variables even when they may not have an **observable impact**. Another (undesirable) consequence of comparing variables is that pure closures (with no state of their own) are not supported. Plus, it requires the compared objects to be of the same runtime type. I rolled my sleeves and created my [own version](https://github.com/beluchin/deepequals) of the deep compare tool that addresses these concerns. – beluchin Feb 26 '16 at 22:09
  • @Wolfgang is there any sample code to direct us to? Where did you pull that quote from? – fIwJlxSzApHEZIl Jul 11 '17 at 22:25
34

I love this question! Mainly because it is hardly ever answered or answered badly. It's like nobody has figured it out yet. Virgin territory :)

First off, don't even think about using equals. The contract of equals, as defined in the javadoc, is an equivalence relation (reflexive, symmetric, and transitive), not an equality relation. For that, it would also have to be antisymmetric. The only implementation of equals that is (or ever could be) a true equality relation is the one in java.lang.Object. Even if you did use equals to compare everything in the graph, the risk of breaking the contract is quite high. As Josh Bloch pointed out in Effective Java, the contract of equals is very easy to break:

"There is simply no way to extend an instantiable class and add an aspect while preserving the equals contract"

Besides what good does a boolean method really do you anyway? It'd be nice to actually encapsulate all the differences between the original and the clone, don't you think? Also, I'll assume here that you don't want to be bothered with writing/maintaining comparison code for each object in the graph, but rather you're looking for something that will scale with the source as it changes over time.

Soooo, what you really want is some kind of state comparison tool. How that tool is implemented is really dependent on the nature of your domain model and your performance restrictions. In my experience, there is no generic magic bullet. And it will be slow over a large number of iterations. But for testing the completeness of a clone operation, it'll do the job pretty well. Your two best options are serialization and reflection.

Some issues you will encounter:

  • Collection order: Should two collections be considered similar if they hold the same objects, but in a different order?
  • Which fields to ignore: Transient? Static?
  • Type equivalence: Should field values be of exactly the same type? Or is it ok for one to extend the other?
  • There's more, but I forget...

XStream is pretty fast and combined with XMLUnit will do the job in just a few lines of code. XMLUnit is nice because it can report all the differences, or just stop at the first one it finds. And its output includes the xpath to the differing nodes, which is nice. By default it doesn't allow unordered collections, but it can be configured to do so. Injecting a special difference handler (Called a DifferenceListener) allows you to specify the way you want to deal with differences, including ignoring order. However, as soon as you want to do anything beyond the simplest customization, it becomes difficult to write and the details tend to be tied down to a specific domain object.

My personal preference is to use reflection to cycle through all the declared fields and drill down into each one, tracking differences as I go. Word of warning: Don't use recursion unless you like stack overflow exceptions. Keep things in scope with a stack (use a LinkedList or something). I usually ignore transient and static fields, and I skip object pairs that I've already compared, so I don't end up in infinite loops if someone decided to write self-referential code (However, I always compare primitive wrappers no matter what, since the same object refs are often reused). You can configure things up front to ignore collection ordering and to ignore special types or fields, but I like to define my state comparison policies on the fields themselves via annotations. This, IMHO, is exactly what annotations were meant for, to make meta data about the class available at runtime. Something like:


@StatePolicy(unordered=true, ignore=false, exactTypesOnly=true)
private List<StringyThing> _mylist;

I think this is actually a really hard problem, but totally solvable! And once you have something that works for you, it is really, really, handy :)

So, good luck. And if you come up with something that's just pure genius, don't forget to share!

Kevin C
  • 373
  • 3
  • 2
20

In AssertJ, you can do:

Assertions.assertThat(expectedObject).isEqualToComparingFieldByFieldRecursively(actualObject);

Probably it won't work in all cases, however it will work in more cases that you'd think.

Here's what the documentation says:

Assert that the object under test (actual) is equal to the given object based on recursive a property/field by property/field comparison (including inherited ones). This can be useful if actual's equals implementation does not suit you. The recursive property/field comparison is not applied on fields having a custom equals implementation, i.e. the overridden equals method will be used instead of a field by field comparison.

The recursive comparison handles cycles. By default floats are compared with a precision of 1.0E-6 and doubles with 1.0E-15.

You can specify a custom comparator per (nested) fields or type with respectively usingComparatorForFields(Comparator, String...) and usingComparatorForType(Comparator, Class).

The objects to compare can be of different types but must have the same properties/fields. For example if actual object has a name String field, it is expected the other object to also have one. If an object has a field and a property with the same name, the property value will be used over the field.

Vlad Dinulescu
  • 1,173
  • 1
  • 14
  • 24
  • 6
    `isEqualToComparingFieldByFieldRecursively` is now deprecated. Use `assertThat(expectedObject).usingRecursiveComparison().isEqualTo(actualObject);` instead :) – dargmuesli May 30 '20 at 20:57
15

Override The equals() Method

You can simply override the equals() method of the class using the EqualsBuilder.reflectionEquals() as explained here:

 public boolean equals(Object obj) {
   return EqualsBuilder.reflectionEquals(this, obj);
 }
Loukan ElKadi
  • 2,687
  • 1
  • 16
  • 20
8

Just had to implement comparison of two entity instances revised by Hibernate Envers. I started writing my own differ but then found the following framework.

https://github.com/SQiShER/java-object-diff

You can compare two objects of the same type and it will show changes, additions and removals. If there are no changes, then the objects are equal (in theory). Annotations are provided for getters that should be ignored during the check. The frame work has far wider applications than equality checking, i.e. I am using to generate a change-log.

Its performance is OK, when comparing JPA entities, be sure to detach them from the entity manager first.

Ryan
  • 101
  • 1
  • 2
5

http://www.unitils.org/tutorial-reflectionassert.html

public class User {

    private long id;
    private String first;
    private String last;

    public User(long id, String first, String last) {
        this.id = id;
        this.first = first;
        this.last = last;
    }
}
User user1 = new User(1, "John", "Doe");
User user2 = new User(1, "John", "Doe");
assertReflectionEquals(user1, user2);
cheffe
  • 9,345
  • 2
  • 46
  • 57
gavenkoa
  • 45,285
  • 19
  • 251
  • 303
  • 2
    especially usefull if you have to handle generated classes, where you do not have any influence about equals! – Matthias B Feb 28 '13 at 15:21
  • 1
    http://stackoverflow.com/a/1449051/829755 mentioned this already. you should have edited that post – user829755 Nov 13 '14 at 09:44
  • 1
    @user829755 In this way I lose points. SO all about point game )) People like get credits for job done, I am too. – gavenkoa Nov 13 '14 at 13:01
4

I am usin XStream:

/**
 * @see java.lang.Object#equals(java.lang.Object)
 */
@Override
public boolean equals(Object o) {
    XStream xstream = new XStream();
    String oxml = xstream.toXML(o);
    String myxml = xstream.toXML(this);

    return myxml.equals(oxml);
}

/**
 * @see java.lang.Object#hashCode()
 */
@Override
public int hashCode() {
    XStream xstream = new XStream();
    String myxml = xstream.toXML(this);
    return myxml.hashCode();
}
4

Hamcrest has the Matcher samePropertyValuesAs. But it relies on the JavaBeans Convention (uses getters and setters). Should the objects that are to be compared not have getters and setters for their attributes, this will not work.

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class UserTest {

    @Test
    public void asfd() {
        User user1 = new User(1, "John", "Doe");
        User user2 = new User(1, "John", "Doe");
        assertThat(user1, samePropertyValuesAs(user2)); // all good

        user2 = new User(1, "John", "Do");
        assertThat(user1, samePropertyValuesAs(user2)); // will fail
    }
}

The user bean - with getters and setters

public class User {

    private long id;
    private String first;
    private String last;

    public User(long id, String first, String last) {
        this.id = id;
        this.first = first;
        this.last = last;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirst() {
        return first;
    }

    public void setFirst(String first) {
        this.first = first;
    }

    public String getLast() {
        return last;
    }

    public void setLast(String last) {
        this.last = last;
    }

}
cheffe
  • 9,345
  • 2
  • 46
  • 57
  • This works great until you have a POJO that is using an `isFoo` read method for a `Boolean` property. There's a PR that's been open since 2016 to fix it. https://github.com/hamcrest/JavaHamcrest/pull/136 – Snekse Aug 28 '17 at 19:21
2

If your objects implement Serializable you can use this:

public static boolean deepCompare(Object o1, Object o2) {
    try {
        ByteArrayOutputStream baos1 = new ByteArrayOutputStream();
        ObjectOutputStream oos1 = new ObjectOutputStream(baos1);
        oos1.writeObject(o1);
        oos1.close();

        ByteArrayOutputStream baos2 = new ByteArrayOutputStream();
        ObjectOutputStream oos2 = new ObjectOutputStream(baos2);
        oos2.writeObject(o2);
        oos2.close();

        return Arrays.equals(baos1.toByteArray(), baos2.toByteArray());
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
Ray Hulha
  • 10,701
  • 5
  • 53
  • 53
1

I think the easiest solution inspired by Ray Hulha solution is to serialize the object and then deep compare the raw result.

The serialization could be either byte, json, xml or simple toString etc. ToString seems to be cheaper. Lombok generates free easy customizable ToSTring for us. See example below.

@ToString @Getter @Setter
class foo{
    boolean foo1;
    String  foo2;        
    public boolean deepCompare(Object other) { //for cohesiveness
        return other != null && this.toString().equals(other.toString());
    }
}   
Breton F.
  • 177
  • 1
  • 6
  • After trying many different classes and libraries for comparing beans this is what I ended up doing. With good toString() implementations this works great. I do however wish there where a standardized way. – Avec Sep 23 '21 at 14:41
1

Your Linked List example is not that difficult to handle. As the code traverses the two object graphs, it places visited objects in a Set or Map. Before traversing into another object reference, this set is tested to see if the object has already been traversed. If so, no need to go further.

I agree with the person above who said use a LinkedList (like a Stack but without synchronized methods on it, so it is faster). Traversing the object graph using a Stack, while using reflection to get each field, is the ideal solution. Written once, this "external" equals() and "external" hashCode() is what all equals() and hashCode() methods should call. Never again do you need a customer equals() method.

I wrote a bit of code that traverses a complete object graph, listed over at Google Code. See json-io (http://code.google.com/p/json-io/). It serializes a Java object graph into JSON and deserialized from it. It handles all Java objects, with or without public constructors, Serializeable or not Serializable, etc. This same traversal code will be the basis for the external "equals()" and external "hashcode()" implementation. Btw, the JsonReader / JsonWriter (json-io) is usually faster than the built-in ObjectInputStream / ObjectOutputStream.

This JsonReader / JsonWriter could be used for comparison, but it will not help with hashcode. If you want a universal hashcode() and equals(), it needs it's own code. I may be able to pull this off with a generic graph visitor. We'll see.

Other considerations - static fields - that's easy - they can be skipped because all equals() instances would have the same value for static fields, as the static fields is shared across all instances.

As for transient fields - that will be a selectable option. Sometimes you may want transients to count other times not. "Sometimes you feel like a nut, sometimes you don't."

Check back to the json-io project (for my other projects) and you will find the external equals() / hashcode() project. I don't have a name for it yet, but it will be obvious.

-1

I guess you know this, but In theory, you're supposed to always override .equals to assert that two objects are truly equal. This would imply that they check the overridden .equals methods on their members.

This kind of thing is why .equals is defined in Object.

If this were done consistently you wouldn't have a problem.

Bill K
  • 62,186
  • 18
  • 105
  • 157
  • 3
    The problem is that I want to automate testing this for a large existing codebase that I didn't write... :) – Uri Sep 30 '09 at 14:06
-1

A halting guarantee for such a deep comparison might be a problem. What should the following do? (If you implement such a comparator, this would make a good unit test.)

LinkedListNode a = new LinkedListNode();
a.next = a;
LinkedListNode b = new LinkedListNode();
b.next = b;

System.out.println(DeepCompare(a, b));

Here's another:

LinkedListNode c = new LinkedListNode();
LinkedListNode d = new LinkedListNode();
c.next = d;
d.next = c;

System.out.println(DeepCompare(c, d));
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
-1

Apache gives you something, convert both objects to string and compare strings, but you have to Override toString()

obj1.toString().equals(obj2.toString())

Override toString()

If all fields are primitive types :

import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return 
ReflectionToStringBuilder.toString(this);}

If you have non primitive fields and/or collection and/or map :

// Within class
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
@Override
public String toString() {return 
ReflectionToStringBuilder.toString(this,new 
MultipleRecursiveToStringStyle());}

// New class extended from Apache ToStringStyle
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.*;

public class MultipleRecursiveToStringStyle extends ToStringStyle {
private static final int    INFINITE_DEPTH  = -1;

private int                 maxDepth;

private int                 depth;

public MultipleRecursiveToStringStyle() {
    this(INFINITE_DEPTH);
}

public MultipleRecursiveToStringStyle(int maxDepth) {
    setUseShortClassName(true);
    setUseIdentityHashCode(false);

    this.maxDepth = maxDepth;
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Object value) {
    if (value.getClass().getName().startsWith("java.lang.")
            || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
        buffer.append(value);
    } else {
        depth++;
        buffer.append(ReflectionToStringBuilder.toString(value, this));
        depth--;
    }
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, 
Collection<?> coll) {
    for(Object value: coll){
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
    }
}

@Override
protected void appendDetail(StringBuffer buffer, String fieldName, Map<?, ?> map) {
    for(Map.Entry<?,?> kvEntry: map.entrySet()){
        Object value = kvEntry.getKey();
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
        value = kvEntry.getValue();
        if (value.getClass().getName().startsWith("java.lang.")
                || (maxDepth != INFINITE_DEPTH && depth >= maxDepth)) {
            buffer.append(value);
        } else {
            depth++;
            buffer.append(ReflectionToStringBuilder.toString(value, this));
            depth--;
        }
    }
}}
Milan
  • 693
  • 5
  • 4