Spring Data Repositories and Interface Segregation

Spring Data Repositories are great. But the interface they provide is huge and they often need to be mocked in unit tests. Fortunately, there is a way to use them without violating the "Interface segregation principle".

While there are lots and lots of principles, patterns and what-not in the programming world, the SOLID principles are among the most useful ones. One of those principles, the "I", which stands for "Interface segregation principle" states the following:

"Many client-specific interfaces are better than one general-purpose interface."

What does this have to do with Spring Data Repositories? Read on and you'll find out!

Spring Data Repositories are a great way to reduce boilerplate code when accessing database resources from inside a Spring Boot application. We just create an interface extending org.springframework.data.jpa.repository.JpaRepository (if we use JPA):

public interface ProductRepository 
    extends JpaRepository<Product, String>
{
    Optional<Product> findByItemNumber(String itemNumber);
}

And just like that, we can use it inside our components:

public class CreateProduct implements CatalogDataCommand<Product>
{
    private final ProductRepository products;

    public CreateProduct(ProductRepository products, /* ... */)
    {
        this.products = products;
    }

    public Result<Product> execute(Action action)
    {
        Optional<Product> existing = products
            .findByItemNumber(action.getItemNumber());
        // ...
    }
}

So far so good. But what if we want to unit test our CreateProduct class? Because ProductRepository is just an interface, there is no implementation to pass when instantiating our class. Spring Data creates an implementation of the repository when bootstrapping the application. But in our unit test there is no application.

We have two options to get an implementation of our repository at this point:

  1. create a test implementation of ProductRepository by hand
  2. use a mocking library like Mockito

The first option is not really feasible because the API of ProductRepository (by extension) is huge. Not just terms of available methods (there are more than 20, which we would need to implement in some way) but in terms of exposed functionality. Remember the I in SOLID? 😭

Fo the second option we just need to add Mockito to our classpath and then we can use it like that:

ProductRepository repository = Mockito.mock(ProductRepository.class);
Mockito
    .when(repository.findByItemNumber(Mockito.anyString())
    .thenReturn(Optional.of(...));

This is better, but still not very nice. The main problem is that we need to know which methods the CreateProduct class will call during the test. If for some reason another of the repository's myriad methods is used instead of findByItemNumber(), the test will fail. This is unfortunate because tests should only fail if we introduced an actual bug.

So how can we improve testability without sacrificing the ease-of-use that the Spring Data Repositories provide?

First, we create an interface that only provides the methods that we use to find our products:

public interface ProductLookup
{
    Optional<Product> findByItemNumber(String itemNumber);
}

In our CreateProduct class, we now require this interface instead of ProductRepository.

public class CreateProduct implements CatalogDataCommand
{
    private final ProductLookup products;

    public CreateProduct(ProductLookup products, /* ... */)
    {
        this.products = products;
    }
}

Writing an implementation for testing is now quite easy:

ProductLookup products = itemNumber -> Optional.of(...);

(Once our lookup interface has more than one method, the code becomes slightly more verbose, but still stays simple to write and use.)

This takes care of the tests. But now our application won't start because there is no bean implementing ProductLookup. To fix this, we need to add our new interface to the existing ProductRepository. That way, the Spring IoC container will inject the repository into our component because it implements the required interface.

public interface ProductRepository 
    extends ProductLookup, JpaRepository<Product, String>
{
}

Et voilà, we have successfully hidden the Spring Data Repository from our CreateProduct class. Interface segregation has been restored and everybody is happy. Drinks all around!

More from Astina

View all articles
Become a driver of the modern B2B economy