Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 340
Lars KJ Avatar asked Nov 19 '17 08:11

Lars KJ


2 Answers

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.

like image 128
Szymon Stepniak Avatar answered Nov 15 '22 10:11

Szymon Stepniak


You can take advantage of Groovy's succinct map comparison syntax:

person1.properties == person2.properties

That only works for simple flat objects, not nested ones. You could adapt it like so:

person1.properties << ['address': person1.address.properties] == person2.properties << ['address': person2.address.properties]

...but JSON solution is more elegant at that point.

like image 23
kat Avatar answered Nov 15 '22 12:11

kat