Search code examples
groovyequalityspock

Is object deep-compare possible with Spock Framework?


How do I check for deep object equality with spock.

Lets say we have a super simple test that compares to identical person objects

def "A persons test"() {
    setup:
    def person1 = new Person("Foo", new Address("Bar"))
    def person2 = new Person("Foo", new Address("Bar"))

    expect:
    person1 == person2
}

The test fails

Condition not satisfied:

person1 == person2
|       |  |
|       |  Person@6bedbc4d
|       false
Person@57af006c

This looks like a very natural way of asserting equality.

One of the main reason to start using spock was to avoid having to write a lot of hamcrest boilerplate matchers code.


Solution

  • Spock has no built-in mechanism for performing deep Object comparison, because defining object equality is out of scope of any testing framework. You can do a various things.

    1. Both classes are Groovy classes

    If both your classes (Person and Address) are Groovy classes you can generate equals and hashCode methods using @EqualsAndHashCode annotation over both classes, like:

    import groovy.transform.EqualsAndHashCode
    import groovy.transform.TupleConstructor
    import spock.lang.Specification
    
    class PersonSpec extends Specification {
    
        def "a person test"() {
            setup:
            def person1 = new Person("Foo", new Address("Bar"))
            def person2 = new Person("Foo", new Address("Bar"))
    
            expect:
            person1 == person2
        }
    
        @TupleConstructor
        @EqualsAndHashCode
        static class Person {
            String name
            Address address
        }
    
        @TupleConstructor
        @EqualsAndHashCode
        static class Address {
            String city
        }
    }
    

    This is just a convenient alternative for implementing both methods in Groovy.

    2. Both classes are Java classes

    If you want to compare both objects with == operator then you will have to define equals and hashCode methods in both classes, something like:

    public final class Person {
    
        private final String name;
        private final Address address;
    
        public Person(String name, Address address) {
            this.name = name;
            this.address = address;
        }
    
        public String getName() {
            return name;
        }
    
        public Address getAddress() {
            return address;
        }
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
    
            Person person = (Person) o;
    
            if (name != null ? !name.equals(person.name) : person.name != null) return false;
            return address != null ? address.equals(person.address) : person.address == null;
        }
    
        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + (address != null ? address.hashCode() : 0);
            return result;
        }
    
        static class Address {
            private final String city;
    
            public Address(String city) {
                this.city = city;
            }
    
            public String getCity() {
                return city;
            }
    
            @Override
            public boolean equals(Object o) {
                if (this == o) return true;
                if (o == null || getClass() != o.getClass()) return false;
    
                Address address = (Address) o;
    
                return city != null ? city.equals(address.city) : address.city == null;
            }
    
            @Override
            public int hashCode() {
                return city != null ? city.hashCode() : 0;
            }
        }
    }
    

    In this example both methods were defined using IntelliJ IDEA "Generate equals and hashCode" command.

    3. I can use Lombok!

    If you don't want to define both methods manually (because e.g. you have to remember to change them anytime you modify your class fields) then you can use Lombok's @EqualsAndHashCode annotation that does something similar to Groovy's annotation, but can be applied to any Java class.

    4. I want to keep default equals and hashCode methods

    Well, in this case you can try various things:

    1. You can try comparing both objects field-by-field, like:

      class PersonSpec extends Specification {
      
          def "a person test"() {
              setup:
              def person1 = new Person("Foo", new Address("Bar"))
              def person2 = new Person("Foo", new Address("Bar"))
      
              expect:
              person1.name == person2.name
      
              and:
              person1.address.city == person2.address.city
          }
      
          @TupleConstructor
          static class Person {
              String name
              Address address
          }
      
          @TupleConstructor
          static class Address {
              String city
          }
      }
      
    2. You can try using 3rd party tools like Unitils reflection assertion

    3. That may sound bizarre, but you can compare JSON representation of both objects, something like:

      import groovy.json.JsonOutput
      import groovy.transform.TupleConstructor
      import spock.lang.Specification
      
      class PersonSpec extends Specification {
      
          def "a person test"() {
              setup:
              def person1 = new Person("Foo", new Address("Bar"))
              def person2 = new Person("Foo", new Address("Bar"))
      
              expect:
              new JsonOutput().toJson(person1) == new JsonOutput().toJson(person2)
          }
      
          @TupleConstructor
          static class Person {
              String name
              Address address
          }
      
          @TupleConstructor
          static class Address {
              String city
          }
      }
      

    Anyway, I would definitely suggest defining equals and hashCode in one way or another and simply use == operator. Hope it helps.