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:
- create a test implementation of
ProductRepository
by hand - use a mocking library like Mockito
The first option is not really feasible because the API of ProductRepository
(by extension) is huge.
Not just in 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? 😭
For 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!