Search
  • Juan Ayala

equals() & hashCode() & AEM Unit Testing

Recently I was reviewing some Java code that I needed to clean up and unit test for an AEM as a Cloud Service migration. It was the usual Sling Models and OSGi components. I came across a POJO that had implemented the equals() and hasCode() methods.

@Override
public int hashCode() {
    // you pick a hard-coded, randomly chosen, non-zero, odd number
    // ideally different for each class
    return new HashCodeBuilder(17, 37)
            .append(field1)
            .append(field2)
            .toHashCode();
}

Over the years I've implemented hashCode & equals. This was the first time I had seen the use of HashCodeBuilder. The equals had been implemented with EqualsBuilder.


In this post, I will go over 2 extra ways to write these methods. And why they may be of use to you as an AEM developer. If you already know how they work, skip to the end.


What do hashCode() and equals() do?


Before we get into how to, let us talk about what they do. There is a plethora of information out there about this. I will not get into much detail about the computer science behind it.


hashCode()


This method is important to data structures such as HashMap, HashSet & HashTable. They will be able to store and lookup keys with greater efficiency, if they have a unique hash code.


equals()


The hash code is a fast way to determine equality. It is not perfect. There is a chance that two objects may produce the same code. This is a collision and each data structure will handle it. equals() will determine true equality.


Key Points between hashCode() & equals()

  • If equals() returns true for two objects, the hashCode() must also be equal.

  • If hashCode() returns the same for two different objects, equals() does not have to return true.

  • If invoked many times during the same execution, hashCode() must return the same value.

Apache Commons Lang 3


Using the Apache Commons Lang 3 library is the first way to write these methods. I already showed you the HashCodeBuilder above. Here is equals()

@Override
public boolean equals(final Object o) {

    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    MyClass myClass = (MyClass) o;
    return new EqualsBuilder().append(field1, myClass.field1)
                              .append(field2, myClass.field2)
                              .isEquals();
}


Google Guava


You can use Guava's Objects utility class

@Override
public boolean equals(final Object o) {

    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    MyClass myClass = (MyClass) o;
    return Objects.equal(field1, myClass.field1)
        && Objects.equal(field2, myClass.field2);
}

@Override
public int hashCode() {
    return Objects.hashCode(field1, field2);
}


Plain Java


In case you are a Java purist and insist on doing everything yourself.

@Override
public boolean equals(final Object o) {

    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    MyClass myClass = (MyClass) o;
    if (field1 != null ? !field1.equals(myClass.field1)
                       : myClass.field1 != null)
        return false;
    return field2 != null ? field2.equals(myClass.field2) 
                          : myClass.field2 == null;
}

@Override
public int hashCode() {
    int result = field1 != null ? field1.hashCode() 
                                : 0;
    result = 31 * result + (field2 != null ? field2.hashCode()
                                           : 0);
    return result;
}

Code Generators


I didn't write any of the samples above. Why spend time fuzzing over boilerplate code? I use IntellJ and it has excellent code generation features. Even though it is easy to generate, there are a couple of annoying side effects. You have to maintain it and make sure it gets covered. This is why my absolute favorite way to generate equals() and hashCode() is with Lombok.


Project Lombok


If you have read my blogs before, you know I love Lombok. My class becomes


@Getter
@EqualsAndHashCode
public static final class MyClass {

    private String field1;
    private String field2;

}

I opted to use @Getter and @EqualsAndHashCode for illustration. Lombok also has @Data and @Value. They combine several other attributes including @Getter and @EqualsAndHashCode.


No maintenance, no coverage, no fuzz.


Why Would I Care as an AEM Developer?


You may be thinking "What boon will I get from implementing these things in AEM projects?" Sling Models and POJOs should be simple, used to transfer data from Sling resources onto a page.


The answer is unit testing. In the past, AEM developers could get away without it. Code coverage and analysis were up to the client to request. With AEM as a Cloud Service, we can no longer escape it. So let us expand on our class, I will call it the Person class.


@Getter
@AllArgsConstructor
@Builder
@NoArgsConstructor
//@EqualsAndHashCode
@Model(adaptables = { Resource.class })
public class Person {

    @ValueMapValue
    private String firstName;
    @ValueMapValue
    private String lastName;
}

I am using a few annotations. I commented out @EqualsAndHashCode for now.


  • @Getter to generate getters for all the class fields.

  • @Builder because I want a nice fluent way to instantiate new objects in the unit test.

  • @AllArgsConstructor because the builder needs a constructor to set all the fields.

  • @NoArgsConstructor for Sling Models. It needs a default constructor to instantiate and then inject the fields.

The JUnit 5 test Using wcm.io AEM Mocks


@ExtendWith(AemContextExtension.class)
class MyTest {

    @Test
    void doTest(final AemContext context) {

        // arrange
        context.addModelsForClasses(Person.class);
        final var resource = context.create()
                                    .resource("/person", "firstName", "John", "lastName", "Doe");

        // act
        final var actual = resource.adaptTo(Person.class);

        // assert
        assertNotNull(actual);
        assertEquals("John", actual.getFirstName());
        assertEquals("Doe", actual.getLastName());
    }
}

Arranging the test is super easy. If this is the first time you are seeing wcm.io AEM Mocks you have been missing out. In one line of code, I set up a test resource with 2 properties.


I adapt the resource to my class and assert. The assertions are not super hard to follow. Imagine if you had more fields. Or if you were asserting a collection of people. Could get pretty ugly.


That is where the builder comes in.


final var expected = Person.builder()
                           .firstName("John")
                           .lastName("Doe")
                           .build();
assertEquals(expected, actual);

This is now easier to read. It won't work until you uncomment @EqualsAndHashCode.


A Little More Collections Please


The world is full of people! Cant have only one. Let us create a new resource to mimic what you may expect from a multi filed. We will use the ResourceBuilder


context.build()
       .resource("/people/items")
       .siblingsMode()
       .resource("item0", "firstName", "Harry", "lastName", "Kim")
       .resource("item1", "firstName", "Wesley", "lastName", "Crusher")
       .resource("item2", "firstName", "Harry", "lastName", "Kim")
       .commit();
final var resource = context.currentResource("/people");

In the People model, we will switch to constructor injection. There we will remove duplicates and sort by the first name.


@Value
@Model(adaptables = { Resource.class })
public class People {

    List<Person> people;

    @Inject
    public People(
            @ChildResource(name = "items")
            final List<Person> people) {

        this.people = people.stream()
                            .distinct()
                            .sorted(Comparator.comparing(Person::getFirstName))
                            .collect(Collectors.toUnmodifiableList());
    }
}


And instead of using JUnit's assertions, let's use AssertJ. It is another useful tool for assertions, especially collections. I will use containsExactly to assert that the duplicate is out and the Ensigns are sorted by the first name.


// act
final var actual = resource.adaptTo(People.class);

// assert
assertThat(actual).isNotNull()
                  .extracting(People::getPeople)
                  .asList()
                  .containsExactly(
                          Person.builder().firstName("Harry").lastName("Kim").build(),
                          Person.builder().firstName("Wesley").lastName("Crusher").build());

Challenge For You


All the assertions so far work because of the equals(). We didn't even get into hash maps. Go ahead and try to use Person as the key in a map i.e. Map<Person, List<Person> aliases>. Try it with and without @EqualsAndHashCode.


Conclusion


By giving objects the ability to compare themselves, we increased the flexibility of the unit testing. As you can see it also allows us to use collection frameworks. In the case of People, I used Java streams to pull out duplicates. And I asserted with AssertJ. This is a good practice if you are developing on the AEMaaCS platform.

286 views0 comments

Recent Posts

See All