Spring Data Repositories und Interface Segregation

Spring Data Repositories sind grossartig. Doch das Interface, das sie bereitstellen, ist riesig, und in Unit-Tests müssen sie oft gemockt werden. Zum Glück gibt es einen Weg, sie zu nutzen, ohne das «Interface segregation principle» zu verletzen.

Es gibt in der Programmierwelt unzählige Prinzipien, Patterns und was nicht noch alles, doch die SOLID-Prinzipien gehören zu den nützlichsten. Eines dieser Prinzipien, das «I», steht für das «Interface segregation principle» und besagt Folgendes:

"Many client-specific interfaces are better than one general-purpose interface." (Viele clientspezifische Interfaces sind besser als ein universelles Interface.)

Was hat das mit Spring Data Repositories zu tun? Lesen Sie weiter, dann erfahren Sie es!

Spring Data Repositories sind ein hervorragendes Mittel, um Boilerplate-Code zu reduzieren, wenn man aus einer Spring-Boot-Applikation heraus auf Datenbankressourcen zugreift. Wir erstellen einfach ein Interface, das org.springframework.data.jpa.repository.JpaRepository erweitert (sofern wir JPA verwenden):

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

Und schon können wir es in unseren Komponenten verwenden:

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 weit, so gut. Was aber, wenn wir unsere Klasse CreateProduct mit einem Unit-Test prüfen wollen? Da ProductRepository nur ein Interface ist, gibt es keine Implementierung, die wir beim Instanziieren unserer Klasse übergeben könnten. Spring Data erzeugt eine Implementierung des Repositorys beim Bootstrapping der Applikation. In unserem Unit-Test gibt es aber keine Applikation.

An diesem Punkt haben wir zwei Möglichkeiten, zu einer Implementierung unseres Repositorys zu kommen:

  1. eine Test-Implementierung von ProductRepository von Hand schreiben
  2. eine Mocking-Library wie Mockito verwenden

Die erste Möglichkeit ist nicht wirklich praktikabel, weil die API von ProductRepository (durch die Vererbung) riesig ist. Nicht nur, was die Anzahl verfügbarer Methoden betrifft (es sind mehr als 20, die wir alle irgendwie implementieren müssten), sondern auch, was den Umfang der bereitgestellten Funktionalität angeht. Erinnern Sie sich an das I in SOLID? 😭

Für die zweite Möglichkeit müssen wir nur Mockito zu unserem Classpath hinzufügen und können es dann so verwenden:

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

Das ist besser, aber immer noch nicht wirklich schön. Das Hauptproblem ist, dass wir wissen müssen, welche Methoden die Klasse CreateProduct während des Tests aufruft. Wird aus irgendeinem Grund statt findByItemNumber() eine andere der unzähligen Methoden des Repositorys verwendet, schlägt der Test fehl. Das ist unglücklich, denn Tests sollten nur dann fehlschlagen, wenn wir tatsächlich einen Bug eingebaut haben.

Wie können wir also die Testbarkeit verbessern, ohne auf die einfache Handhabung zu verzichten, die uns die Spring Data Repositories bieten?

Zuerst erstellen wir ein Interface, das nur die Methoden bereitstellt, mit denen wir unsere Produkte finden:

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

In unserer Klasse CreateProduct verlangen wir nun dieses Interface anstelle von ProductRepository.

public class CreateProduct implements CatalogDataCommand
{
    private final ProductLookup products;

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

Eine Implementierung für Tests zu schreiben, ist jetzt ganz einfach:

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

(Sobald unser Lookup-Interface mehr als eine Methode hat, wird der Code etwas ausführlicher, bleibt aber dennoch einfach zu schreiben und zu verwenden.)

Damit sind die Tests erledigt. Jetzt startet aber unsere Applikation nicht mehr, weil es keine Bean gibt, die ProductLookup implementiert. Um das zu beheben, fügen wir unser neues Interface dem bestehenden ProductRepository hinzu. So injiziert der Spring-IoC-Container das Repository in unsere Komponente, weil es das benötigte Interface implementiert.

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

Et voilà, wir haben das Spring Data Repository erfolgreich vor unserer Klasse CreateProduct verborgen. Die Interface Segregation ist wiederhergestellt, und alle sind glücklich. Eine Runde Drinks für alle!

Mehr von Astina

Alle Artikel