Demystifying Sorting Assertions With AssertJ

Demystifying Sorting Assertions With AssertJ

There are times when a new feature containing sorting is introduced. Obviously, we want to verify that the implemented sorting works correctly. AssertJ framework provides first-class support for such tasks. This article shows how to write such tests.

In this article, you will learn the following:

  • Two main methods provided by Assert frameworks for sorting assertion
  • How to assert data sorted in ascending or descending way
  • How to assert data sorted by multiple attributes
  • How to deal with nulls or case-insensitive sorting

Introduction

First of all, we need to know that AssertJ provides the AbstractListAssert abstract class for asserting any List argument. This class also contains the isSorted and isSortedAccordingTo methods for our purpose. The solution used there is based on the Comparator interface and its several static methods. Before moving to our examples, let’s have a short introduction to the data and technology used in this article. 

Almost every case presented here is demonstrated with two examples. The simple one is always presented first, and it’s based on a list of String values. The goal is to provide the simplest example which can be easily taken and tested. However, the second example is based on the DB solution introduced in the Introduction: Querydsl vs. JPA Criteria article. There, we use Spring Data JPA solution for the PDM model defined as:

Spring Data JPA solution for the PDM model

These tables are mapped to City and Country entities. Their implementation is not mentioned here, as it’s available in the article mentioned earlier. Nevertheless, the data used in the examples below are defined like this:

[
    City(id=5, name=Atlanta, state=Georgia, country=Country(id=3, name=USA)),
    City(id=13, name=Barcelona, state=Catalunya, country=Country(id=7, name=Spain)),
    City(id=14, name=Bern, state=null, country=Country(id=8, name=Switzerland)),
    City(id=1, name=Brisbane, state=Queensland, country=Country(id=1, name=Australia)),
    City(id=6, name=Chicago, state=Illionis, country=Country(id=3, name=USA)),
    City(id=15, name=London, state=null, country=Country(id=9, name=United Kingdom)),
    City(id=2, name=Melbourne, state=Victoria, country=Country(id=1, name=Australia)),
    City(id=7, name=Miami, state=Florida, country=Country(id=3, name=USA)),
    City(id=4, name=Montreal, state=Quebec, country=Country(id=2, name=Canada)),
    City(id=8, name=New York, state=null, country=Country(id=3, name=USA)),
    City(id=12, name=Paris, state=null, country=Country(id=6, name=France)),
    City(id=11, name=Prague, state=null, country=Country(id=5, name=Czech Republic)),
    City(id=9, name=San Francisco, state=California, country=Country(id=3, name=USA)),
    City(id=3, name=Sydney, state=New South Wales, country=Country(id=1, name=Australia)),
    City(id=10, name=Tokyo, state=null, country=Country(id=4, name=Japan))
]

Finally, it’s time to move to our examples. Let’s start with a simple isSorted method.

isSorted Method

AssertJ framework provides the isSorted method in order to verify values that implement the Comparable interface, and these values are in a natural order. The simplest usage can be seen in the dummyAscendingSorting test as:

  • Define sorted values (line 3)
  • Assert the correct order with isSorted method (line 5)
@Test
void dummyAscendingSorting() {
	var cities = List.of("Atlanta", "London", "Tokyo");
  
	assertThat(cities).isSorted();
}

Now, let’s imagine a more real-like example where the data is provided by Spring Data JPA. This use case is demonstrated in the sortingByNameAscending test as:

  • Define a pagination request for loading data. Here, we request data sorted just by city name and the page with the size of 5 (line 5).
  • Load cities from cityRepository with the findAll method (line 5).
  • Assert the loaded data as:
    • Check the number of cities returned by the search (line 10) -> to be equal to the requested page size.
    • Extract a name attribute from the City entity (line 11). This is necessary as our City entity doesn’t implement the Comparable interface. More details are covered at the end of this article.
    • Assert the correct order with isSorted (line 12) -> this is our goal.
import static org.springframework.data.domain.Sort.Direction.ASC;

@Test
void sortingByNameAscending() {
	var pageable = PageRequest.of(0, 5, ASC, City_.NAME);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.hasSize(5)
		.map(City::getName)
		.isSorted();
}

Reverse Order

In some cases, we use descending order. The sorting assertion can be handled in a pretty similar way, but we need to use the isSortedAccordingTo method instead of the isSorted method. This method is used for the advanced sorting assertion. 

The simplest assertion for values sorted in descending ways can be seen in the dummyDescendingSorting test. This is the same as the usage of the isSorted method, but this time, we need to use the already-mentioned isSortedAccordingTo method with the  Collections.reverseOrder comparator.

import static java.util.Collections.reverseOrder;

@Test
void dummyDescendingSorting() {
	assertThat(List.of("Tokyo", "London", "Atlanta")).isSortedAccordingTo( reverseOrder() );
}

The real-like solution is demonstrated by the sortingByNameDescending test. It’s very similar to the previous sortingByNameAscending test, but this time, we use data loaded from DB.

import static java.util.Collections.reverseOrder;
import static org.springframework.data.domain.Sort.Direction.DESC;

@Test
void sortingByNameDescending() {
	var pageable = PageRequest.of(0, 5, DESC, City_.NAME);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.hasSize(5)
		.map(City::getName)
		.isSortedAccordingTo( reverseOrder() );
}

Custom Comparator

Sometimes, we use sorting by multiple attributes. Therefore, we cannot use the simple approach shown in previous examples. For asserting sorting by multiple attributes, we need to have a comparator. This case is demonstrated in the sortingByCountryAndCityNames test as:

  • Define a pagination request with ascending sorting first by the country name and then by the city name (line 4). Now, we use a higher page size in order to load all available data.
  • Assert the loaded data as:
    • Assert the correct order by the country name (line 4) with the custom comparator implemented in the getCountryNameComparator method (lines 13-15).
    • Assert the correct order by the city name (line 10) simply by providing the desired function to the Comparator.thenComparing method.
@Test
void sortingByCountryAndCityNames() {
	var countryNameSorting = City_.COUNTRY + "." + Country_.NAME;
	var pageable = PageRequest.of(0, 15, ASC, countryNameSorting, City_.NAME);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.isSortedAccordingTo( getCountryNameComparator()
                .thenComparing( City::getName ));
}

private Comparator getCountryNameComparator() {
	return ( c1, c2 ) -> c1.getCountry().getName().compareTo(c2.getCountry().getName());
}

In order to promote your understanding, the data by Spring Data JPA loaded in the sortingByCountryAndCityNames test is listed below as:

[
    City(id=1, name=Brisbane, state=Queensland, country=Country(id=1, name=Australia)),
    City(id=2, name=Melbourne, state=Victoria, country=Country(id=1, name=Australia)),
    City(id=3, name=Sydney, state=New South Wales, country=Country(id=1, name=Australia)),
    City(id=4, name=Montreal, state=Quebec, country=Country(id=2, name=Canada)),
    City(id=11, name=Prague, state=null, country=Country(id=5, name=Czech Republic)),
    City(id=12, name=Paris, state=null, country=Country(id=6, name=France)),
    City(id=10, name=Tokyo, state=null, country=Country(id=4, name=Japan)),
    City(id=13, name=Barcelona, state=Catalunya, country=Country(id=7, name=Spain)),
    City(id=14, name=Bern, state=null, country=Country(id=8, name=Switzerland)),
    City(id=5, name=Atlanta, state=Georgia, country=Country(id=3, name=USA)),
    City(id=6, name=Chicago, state=Illionis, country=Country(id=3, name=USA)),
    City(id=7, name=Miami, state=Florida, country=Country(id=3, name=USA)),
    City(id=8, name=New York, state=null, country=Country(id=3, name=USA)),
    City(id=9, name=San Francisco, state=California, country=Country(id=3, name=USA)),
    City(id=15, name=London, state=null, country=Country(id=9, name=United Kingdom))
]

Sorting With NULLs

Some data might contain a null value, and we need to deal with it. This case is covered in the dummyAscendingSortingWithNull test as:

  • Define data with null value in the beginning (line 6).
  • Assert null value in the beginning with Comparator.nullsFirst comparator and the ascending order by using Comparator.naturalOrder comparator.
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;

@Test
void dummyAscendingSortingWithNull() {
	assertThat(Arrays.asList(new String[] { null, "London", "Tokyo" }))
		.isSortedAccordingTo(nullsFirst(naturalOrder()));
}

The same approach in our real-like solution is available in the sortingByStateAscending test.

import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;

@Test
void sortingByStateAscending() {
	var pageable = PageRequest.of(0, 15, ASC, City_.STATE);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.map(City::getState)
		.isSortedAccordingTo(nullsFirst(naturalOrder()));
}

It is also possible to receive a null at the end instead of the beginning. Let’s see this case in our last example.

A Complex Sorting Example

Our last example demonstrates a more complex scenario. Our goal is to verify the order of our data sorted in descending and case-insensitive order. Additionally, this data contains null values. The simple usage is in the dummyDescendingSortingWithNull test as:

  • Define sorted values (line 7).
  • Assert the correct order with isSortedAccordingTo(line 8) and 
    • Comparator.nullsLast – to check that nulls are at the end -> as we have descending sorting,
    • Collections.reverseOrder – to check the descending order and
    • String.CASE_INSENSITIVE_ORDER – to compare values ignoring the case sensitivity.
import static java.util.Collections.reverseOrder;
import static java.util.Comparator.nullsLast;
import static String.CASE_INSENSITIVE_ORDER;

@Test
void dummyDescendingSortingWithNull() {
	assertThat(Arrays.asList(new String[] { "London", "atlanta", "Alabama", null}))
		.isSortedAccordingTo(nullsLast(reverseOrder(CASE_INSENSITIVE_ORDER)));
}

The same approach in our real-like solution is available in sortingByStateDescending test.

import static java.util.Collections.reverseOrder;
import static java.util.Comparator.nullsLast;
import static String.CASE_INSENSITIVE_ORDER;

@Test
void sortingByStateDescending() {
	var pageable = PageRequest.of(0, 15, DESC, City_.STATE);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.map(City::getState)
		.isSortedAccordingTo(nullsLast(reverseOrder(CASE_INSENSITIVE_ORDER)));
}

Known Pitfall

When dealing with sorting, it’s easy to forget we can apply sorting functions only to instances implementing the Comparable interface. In our case, the City entity doesn’t implement this interface. The appropriate comparator depends on our sorting. Therefore, the comparator can be different for every sortable attribute or their combinations. Let’s demonstrate this situation from our first example by the failByNotProvidingCorrectComparator test as:

import static org.springframework.data.domain.Sort.Direction.ASC;

@Test
void failByNotProvidingCorrectComparator() {
	var pageable = PageRequest.of(0, 5, ASC, City_.NAME);
	
	Page page = cityRepository.findAll(pageable);

	assertThat(page.getContent())
		.hasSize(5)
		// .map(City::getName)
		.isSorted();
}

We get the some elements are not mutually comparable in group error when the map function is commented out (line 11).

java.lang.AssertionError: 
some elements are not mutually comparable in group:
  [City(id=5, name=Atlanta, state=Georgia, country=Country(id=3, name=USA)),
    City(id=13, name=Barcelona, state=Catalunya, country=Country(id=7, name=Spain)),
    City(id=14, name=Bern, state=null, country=Country(id=8, name=Switzerland)),
    City(id=1, name=Brisbane, state=Queensland, country=Country(id=1, name=Australia)),
    City(id=6, name=Chicago, state=Illionis, country=Country(id=3, name=USA))]
	at com.github.aha.sat.jpa.city.CityRepositoryTests$FindAll.sortingByNameAscending(CityRepositoryTests.java:71)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1597)
}

Such simplification is wrong, but it can happen from time to time when we try to simplify our code.

Summary and Source Code

First, the article explained the basics of sorting with the isSorted method. Next, sorting assertions for data in reverse order and sorting by two criteria using the custom comparator were demonstrated.
After that, the sorting for data with null values was covered. Finally, the pitfall related to the misuse of sorting assertions provided by the AssertJ framework was explained.

The complete source code presented above is available in my GitHub repository.

About sujan

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.