Demystifying Sorting Assertions With AssertJ
Know the options for sorting assertions in AssertJ, including ascending/descending order, multiple attributes, null handling, and case-insensitive sorting.
Join the DZone community and get the full member experience.
Join For FreeThere 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
null
s 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:
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 thefindAll
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 theCity
entity (line 11). This is necessary as ourCity
entity doesn't implement theComparable
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<City> 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<City> 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.
- Assert the correct order by the country name (line 4) with the custom comparator implemented in the
@Test
void sortingByCountryAndCityNames() {
var countryNameSorting = City_.COUNTRY + "." + Country_.NAME;
var pageable = PageRequest.of(0, 15, ASC, countryNameSorting, City_.NAME);
Page<City> page = cityRepository.findAll(pageable);
assertThat(page.getContent())
.isSortedAccordingTo( getCountryNameComparator()
.thenComparing( City::getName ));
}
private Comparator<City> 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 withComparator.nullsFirst
comparator and the ascending order by usingComparator.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<City> 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) andComparator.nullsLast
– to check that nulls are at the end -> as we have descending sorting,Collections.reverseOrder
– to check the descending order andString.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<City> 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<City> 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.
Opinions expressed by DZone contributors are their own.
Comments