How to test the composition of Spring Data Specifications


In my blog post ‘How to use Spring Data’s Specification’ I showed you how you can write automated tests for your Spring Data Specifications. Although it explained how you can test a single specification, it missed a crucial part. The testing of composite specifications. In this follow-up post, I will close the gap left last time.

The Example

In the aforementioned post, we created two specifications. One is a predicate of a restaurant’s name and one is the predicate of a restaurant’s rating. To demonstrate the flexibility we get from specifications, we also created the RestaurantService which creates a composite specification by combining both single specifications with an ‘and’.

What do we have to test?

We already proved that our single specifications work in isolation by implementing @DataJpaTests. What’s still left is to test the correct composition of the single specifications. Are they combined in the way we expect them to be combined? To answer this question we need to test the component that does the composition - the RestaurantService.

RestaurantService.java
@Service
public class RestaurantService {

    //...

    public List<Restaurant> findTopRatedByName(String name) {
        return restaurantRepository.findAll(where(nameIsLike(name)).and(hasTopRating()));
    }
}
  • We used the and method of the Specification interface, to combine both of our specifications. We still need to test that this combination matches our expectations.

How do we test the composition?

The best way I could find to test the correct composition of the specifications is to use an integration test. The test uses the component that does the composition and checks its response.

RestaurantServiceTest.java
@DataJpaTest
class RestaurantServiceTest {

    @Autowired
    private RestaurantRepository restaurantRepository;

    @BeforeEach
    void setup() {
        final var cafeJava = new Restaurant("Café Java");
        cafeJava.rate(10);
        restaurantRepository.save(cafeJava);

        final var cafeSpring = new Restaurant("Café Spring");
        cafeSpring.rate(7);
        restaurantRepository.save(cafeSpring);

        final var restaurantSpring = new Restaurant("Spring Restaurant");
        restaurantSpring.rate(10);
        restaurantRepository.save(restaurantSpring);
    }

    @Test
    void findsTopRatedRestaurantsByName() {
        final var cut = new RestaurantService(restaurantRepository);
        final var restaurants = cut.findTopRatedByName("Spring");

        assertThat(restaurants, hasSize(1));
        assertThat(restaurants, contains(hasProperty("name", is("Spring Restaurant"))));
    }
}
  • We use a @DataJpaTest to get the RestaurantRepository configured. It is needed to persist some test data and retrieve the data matching our composite specification.
  • In the setup method, we persist three restaurants. Two of them (Café Java and Café Spring) should not be retrieved because they match only partially with the composite specification. The last one (Spring Restaurant) should be retrieved.
  • The RestaurantService is not configured for us because it is not covered by the @DataJpaTest test slice. We need to instantiate it manually.
  • We call the method that composes our single specifications. It should return all top-rated (average rating >= 8) Restaurants with “Spring” in their name.
  • We use hamcrest to make some assertions about the result of our method. Because we retrieved just the restaurant that matched both parts of the composite specification we proved that they are connected with an “and” logic.

Why don’t we use a Unit Test?

Sadly, I don’t know of any way to test the composition of multiple specifications in a unit test. The default methods inside the Specification (where, and, or, not) always return a Specification. Therefore, I don’t see any possibility to make assertions about the elements that build the composite specification or how they are combined.
Due to the lack of possibilities (or knowledge), I have chosen to use an integration test and use the retrieved data as a derivation of the composite logic.

Resources