Raimund Krämer

Software Craftsman, Consultant, Coach

I learned about EqualsVerifier from a technical coach I know. A few months ago we met (not for the first time) at a conference we both spoke at, and after a collaborative hands-on session in ATDD (acceptance-test-driven development) we talked about TDD and ATDD, and how these practices lead to high code coverage almost as a side effect, with the main purposes of course being maintainable, testable design and a mutual understanding of the problem.

When we got to the topic of boilerplate code (you know, the remaining 20 percent of code that takes 80 percent of effort to get test coverage for), he told me about EqualsVerifier, a library for (unit) testing the equals and hashCode contract in Java. I had not heard about that before, but I have since then tried it out and am considering suggesting it for a client’s project, because it solves a problem we’ve been having with very little effort.

As it turns out, there are other similar libraries for that in Java, but EqualsVerifier seems to be the most popular, most modern one today. It has also been around for over 15 years, but still has been regularly updated for modern Java features.

  1. Why would you want to test equals and hashCode?
  2. A minimal example using EqualsVerifier
  3. It works with records
  4. It works with Lombok
  5. It supports Kotlin
  6. It can be easily set up for multiple classes or a whole package
  7. Conclusion

Why would you want to test equals and hashCode?

equals and hashCode in Java need to follow a strict contract. Most Java developers are probably aware of it, but when coming from other languages, it might not be obvious. The contract is well documented in the Javadoc of both methods, but the compiler does not (and cannot) enforce it. Modern language features like records, tools like Lombok, and static analysis might help, but are not a universal solution for the problem.

As a quick refresher, the contract looks like this:

  • The equals() relationship between the two operands is an equivalence relation (i.e., transitive, symmetrical, reflexive), and it must be consistent across multiple invocations.
  • The hashCode() of two equal objects must be the same (although two unequal objects do not necessarily need to have different hash codes).

This contract is not just a suggestion, breaking the contract can lead to subtle but potentially severe bugs or security problems. For example, data structures like HashMap and HashSet rely on two equal objects (or keys) having the same hashCode in order to retrieve values or prevent duplicates, and on those hashCodes not changing between invocations, ideally guaranteed via immutability.

Even though you rarely need to implement these methods manually and would instead generate them with your IDE, when an attribute is later added to a class it can easily be forgotten to update equals and hashCode. Similarly, developers who are not aware of the contract, or lack understanding of the underlying mechanisms of hashCode, might delete the hashCode method just because they are never actively calling it. I’ve personally seen both of these scenarios happen multiple times. I’ve also heard stories of inexperienced developers implementing equals by returning the hash code.

I hope this demonstrates that having tests for equals and hashCode (and the contract involving both) would at least be valuable, but what about the ROI of writing those tests? Since these methods are typically not considered worth testing by themselves (and note that one test per method is generally an anti-pattern), if there is a bug involving those methods, it would probably be caught (if at all) by one of the slower, less often executed, harder to maintain integration or system tests ‒ and only if the exact scenario actually happens to be covered by one of those. Wouldn’t it be great if we had a simple way of verifying equals and hashCode in fast unit tests without having to write boilerplate test code?

A minimal example using EqualsVerifier

Lets imagine a simple Java class with two attributes and an equals method. (The code examples are also on GitHub: https://github.com/kraemer-raimund/equalsverifier-example)

final class Book {

    private final String author;
    private final String title;

    public Book(String author, String title) {
        this.author = author;
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public String getTitle() {
        return title;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Book book)) {
            return false;
        }
        return Objects.equals(author, book.author)
                && Objects.equals(title, book.title);
    }
}

All I need to do to test the equals-hashCode contract is this oneliner:

import books.Book;
import nl.jqno.equalsverifier.EqualsVerifier;
import org.junit.jupiter.api.Test;

public class ExamplesTest {

    @Test
    void equalsHashCodeIsCorrect() {
        EqualsVerifier.forClass(Book.class).verify();
    }
}

You may or may not have already realized that the Book class above implements equals but not hashCode. This is the error message I get from the test:

java.lang.AssertionError: EqualsVerifier found a problem in class books.Book.
-> hashCode: hashCodes should be equal:
  books.Book@24c4ddae (616881582)
and
  books.Book@37fb0bed (939199469)

I generate the missing hashCode method and the test is green:

    @Override
    public int hashCode() {
        return Objects.hash(author, title);
    }

It works with records

If I replace the class Book above with a record, it becomes this simple oneliner:

record Book(String author, String title) { }

The test is still green. Now one might conclude that we can delete the test since equals and hashCode are implicitly implemented by the record, however that is not (necessarily) true for a few reasons:

  • The test only specifies that equals and hashCode must be implemented correctly, but using a record is just one of the ways of doing so. It’s an implementation detail.
  • Even in records, equals and hashCode can be overridden; even though they usually don’t need to be, in some cases a custom implementation might be necessary.
  • Nothing stops a future developer from implementing equals and hashCode in a record (and doing so incorrectly) simply because they didn’t know that they don’t need to.
  • Someone could decide to replace the record with a class, simply because they need to make the type mutable for some reason.

It works with Lombok

It shouldn’t be surprising that equals and hashCode generated by Lombok can also be verified, since EqualsVerifier probably doesn’t even “know” that Lombok is used, it only sees the resulting generated type. However, there are still some Lombok-specific things worth mentioning that can be handled one way or another using EqualsVerifier.

If we use a Lombok value class (roughly comparable to a record), the test as written in previous examples is simply green.

package books;

import lombok.Value;

@Value
final class Genre {

    private String name;
    private int numberOfBooks;
}
    @Test
    void equalsHashCodeIsCorrect() {
        EqualsVerifier.forClass(Book.class).verify();
        EqualsVerifier.forClass(Genre.class).verify();
    }

However, if we use @Data instead of @Value (the mutable version of a @Value class or record), the test fails because due to the mutability one or both of the compared objects might change between invocations. To get the test green, we need to either make the class immutable (my preferred option whenever possible) or use a more lenient EqualsVerifier, like one of the following, the second one being stricter and more explicit regarding what it’s less strict about.

EqualsVerifier
        .simple()
        .forClass(Genre.class)
        .verify();

EqualsVerifier
        .forClass(Genre.class)
        .suppress(Warning.NONFINAL_FIELDS)
        .verify();

It supports Kotlin

Although I have not tested it with Kotlin yet, I’ve seen Kotlin-specific source code and tests in EqualsVerifier’s source on GitHub, as well es Kotlin-specific GitHub issues. Not only can it be used from tests written in Kotlin due to the inherent compatibility between Kotlin and Java (especially on the Kotlin side), it also seems to support Kotlin-specific language features like properties and backing fields that don’t have an exact Java equivalent.

It can be easily set up for multiple classes or a whole package

We want to reduce boilerplate, so it’s great that in addition to it being a oneliner, we don’t even need to add that oneliner test for each class. Maybe one per package/module/project is enough, depending on project size, so we have a single test (or a few tests) to cover all of the relevant equals and hashCode implementations.

    @Test
    void equalsHashCodeIsCorrectInMultipleClasses() {
        EqualsVerifier
                .forClasses(Author.class, Book.class, Genre.class)
                .verify();
    }

Alternatively, for a larger number of classes:

    private static final Iterable<Class<?>> classesToCheck = List.of(
            Author.class,
            Book.class,
            Genre.class
            // ...
    );

    @Test
    void equalsHashCodeIsCorrectInMultipleClasses() {
        EqualsVerifier.forClasses(classesToCheck).verify();
    }

If we want to, we can even set it up for a whole package and have it automatically search for all applicable classes in that package. However, if you’re in a large legacy code base and you actually find many test failures for that package the first time you run it, it might make sense to add the classes one by one and explicitly pass them as a list, so that you can iron out the problems incrementally, class by class, without having to touch many classes in a single big bang fix.

    @Test
    void equalsHashCodeIsCorrectInTheWholePackage() {
        EqualsVerifier.forPackage("books").verify();
    }

Conclusion

EqualsVerifier can greatly reduce boilerplate compared to writing tests for equals and hashCode, or more likely, it helps us to actually test them if we otherwise wouldn’t. Since EqualsVerifier is very strict by default (in my opinion a good thing when using it from the start), it may seem too strict when introducing it in legacy code, but it provides a “simple” verifier that is far more lenient than the default and lots of configuration options to suppress specific warnings when needed.