How to AssertThat Two Objects Are Equal

What does it mean that two objects are equal?

4 minute read

The answer to this question is easy:

assertThat(a, is(b))

Done.

Wait, before closing this web page, let me ask you a few questions.

First, let’s make a concrete class Student.

public class Student {
    public final String name;
    public final int age;
    public final String id;

    public Student(String name, int age, String id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }
}

Very simple data class. Now let’s try the solution in the beginning of the article.

@Test
public void test() {
    Student bobFromApi = new Student("Bob", 18, "#123");
    Student bobFromDb = new Student("Bob", 18, "#123");
    assertThat(bobFromApi, is(bobFromDb));
}

It failed! What’s wrong with the test? Nothing is wrong with the test, it’s in the Student class. Since you are a smart boy/girl, you might have guessed it already, yes, we have to override equals() and hashCode(). We have to tell the computer what it means that two students are equal, otherwise computer thinks that the 2 students exists in different memory addresses so they are different.

Now let’s try again by adding the following code to the Student class.

@Override
public boolean equals(Object o) {
    if (this == o) return true;

    if (o == null || getClass() != o.getClass()) return false;

    Student student = (Student) o;

    return new EqualsBuilder()
            .append(age, student.age)
            .append(name, student.name)
            .append(id, student.id)
            .isEquals();
}

@Override
public int hashCode() {
    return new HashCodeBuilder(17, 37)
            .append(name)
            .append(age)
            .append(id)
            .toHashCode();
}

Here we are using some helper functions from apache common library.

Let’s run the test again.

OK, it passed.

End of stor…

Wait! Let’s examine the equals and hashCode methods we just wrote. They are not quite true. For example, what if Bob decided to change his name to Bobby, is he still the same person? So actually the student id is the one and only true identifier. So let’s rewrite them as:

@Override
public boolean equals(Object o) {
    if (this == o) return true;

    if (o == null || getClass() != o.getClass()) return false;

    Student student = (Student) o;

    return new EqualsBuilder()
            .append(id, student.id)
            .isEquals();
}

@Override
public int hashCode() {
    return new HashCodeBuilder(17, 37)
            .append(id)
            .toHashCode();
}

Then there is a problem if we have test like this:

@Test
public void test() {
    Student bobFromApi = new Student("Bob", 18, "#123");
    Student bobFromDb = new Student("Bobby", 18, "#123");
    assertThat(bobFromApi, is(bobFromDb));
}

This will pass, but we really want it to fail. Because we are testing that our api and database returns the same result.

This reveals a practical problem that sometimes we want to have different equality tests in production code and testing code. In the above example, we just want to test two Student objects, one from api and one from database, to see whether they have identical fields.

Here I provide 2 good solutions to this problem.

  • Use samePropertyValuesAs together with Java Beans

Don' confuse the Java Beans here with the one used for web development. Here it is only a convention: all fields are private and use getters and setters. More on it can be found here.

That means we have to make some tweaks to our Student class.

// A Java Bean Object class
public class Student {
    private  String name;
    private  int age;
    private final String id;

    public Student(String name, int age, String id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public String getId() {
        return id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("name", name)
                .append("age", age)
                .append("id", id)
                .toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;

        if (o == null || getClass() != o.getClass()) return false;

        Student student = (Student) o;

        return new EqualsBuilder()
                .append(id, student.id)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 37)
                .append(id)
                .toHashCode();
    }
}

Or if you don’t want to modify the class, you can do the second way below.

  • Use JAssert

It has a built in matcher

assertThat(actual).isEqualToComparingFieldByField(expected);

~The End~

  •   Category
  • TDD
comments powered by Disqus