Testen von Spring Data Repositories

“So if you want to go fast, if you want to get done quickly, if you want your code to be easy to write, make it easy to read.”

Robert C. Martin

Spring Data Repositories sind eine ideale Abstraktion der Persistenz eigener Applikationen. Durch die Definition eines Java Interfaces werden alle notwendigen Informationen bereitgestellt, um Entitäten zu speichern und später wieder zu laden.

@Repository
public interface FacilityRepository extends Repository<Facility, Long> {
  Stream<Facility> findAll();
  Facility findById(Long id);
  Facility save(Facility facility);
}

Das obige Beispiel stellt ein Repository bereit, in dem Entitäten vom Typ Facility gespeichert werden können. Laden kann man entweder alle vorhandenen Entitäten über die Methode findAll oder eine einzelne Eintität über findById. Entität und Primärschlüssel werden über die generischen Parameter des Repository Interface definiert. In diesem Fall Facility und Long.

Beim Unit Testen eines Spring Boot Service der obiges FacilityRepository verwendet gibt es zwei nahe liegende Möglichkeiten. Entweder ein Spring Boot Test oder ein Mockito Test. Neben diesen gibt es aber mit
Spring Data Mock noch eine dritte interessante Alternative .

@SpringBootTest
class FacilityRepositoryTest {
  @Autowired
  private FacilityRepository repository;

  @Test
  void test() {
    Address address = new Address();
    address.setCity("Bloorgate");
    Facility facility = new Facility();
    facility.setName("Bloor").setAddress(address);

    Facility savedEntity = repository.save(facility);

    assertEquals("Bloorgate", repository.findById(savedEntity.getId()).getAddress().getCity());
  }
}

Der Spring Boot Test nutzt das Dependency Injection von Spring um das Repository zu erstellen. Alle Aktionen auf dem Repository werden dabei standardmäßig auf einer H2 Datenbank durchgeführt. Obwohl die Datenbank leichtgewichtig und schnell ist, hat dieser Ansatz einen nicht unerheblichen Nachteil. Damit der Test durchgeführt werden kann, muss ein Spring Kontext gestartet werden. Die Tests dauern so erheblich länger, als alternative Ansätze.

@ExtendWith(MockitoExtension.class)
class FacilityRepositoryTest3 {
  @Mock
  private FacilityRepository repository;

  @Test
    void test() {
    Address address = new Address();
    address.setCity("Bloorgate");

    Facility facility = new Facility();
    facility.setId(1L);
    facility.setName("Bloor").setAddress(address);

    when(repository.findById(1L)).thenReturn(facility);
    when(repository.save(any())).thenReturn(facility);

    Facility savedEntity = repository.save(facility);

    assertEquals("Bloorgate", repository.findById(savedEntity.getId()).getAddress().getCity());
  }
}

Der Mockito Test sieht dem Spring Boot Test recht ähnlich. Statt einen Spring Kontext zu erstellen und das Repository per @Autowired zu initialisieren, wird ein Mock-Objekt für das Repository erstellt. Dieser Test ist erheblich schneller, weil kein Kontext erstellt werden muss. Dafür haben Mockito Tests einen anderen Nachteil. Damit das Mock korrekt arbeitet müssen die Tests das Verhalten des Repositories definieren.

when(repository.findById(1L)).thenReturn(facility);
when(repository.save(any())).thenReturn(facility);

Hier ist es einmal das Verhalten für die Methoden findbyId und save. Das sorgt nicht nur für eine Menge zusätzlichen Code in den Test-Methoden, es birgt auch die Gefahr ein falsches Verhalten zu definieren. Wer das Mocken einer save Methode vergisst, sorgt ggf. für NULL-Tests im produktiven Code. In diesem Beispiel sieht man außerdem, dass in beiden Fällen das identische Objekt verwendet wird, dem der Autor aus Faulheit eine initiale ID gegönnt hat. Bei einem initialen Speichern besitzt die Entität aber noch keine ID. Ein weiteres Einfallstor für Fehler im Programm.

Eine weitere einfache Möglichkeit für Unit Tests mit Repositories liefert das Projekt spring-data-mock. Es ermöglicht Tests ohne die Nachteile der beiden anderen Ansätze. Wie der Mockito Ansatz benötigt es keinen Spring Kontext, das Mock wird aber durch eine In-Memory Implementierung für Repositories ersetzt. Eingebunden wird das Projekt über folgende Maven Dependency.

<dependency>
    <groupId>com.mmnaseri.utils</groupId>
    <artifactId>spring-data-mock</artifactId>
    <version>${spring-data-mock.version}</version>
    <scope>test</scope>
</dependency>

Das Projekt stellt die Klasse RepositoryFactoryBuilder bereit. Diese Klasse stellt konkrete Implementierungen für die Repositories bereit.

class FacilityRepositoryTest2 {
  private FacilityRepository repository;
	
  @BeforeEach
  void setUp() {
    repository = RepositoryFactoryBuilder.builder().mock(FacilityRepository.class);
  }

  @Test
  void test() {
    Address address = new Address();
    address.setCity("Bloorgate");
    Facility facility = new Facility();
    facility.setName("Bloor").setAddress(address);

    Facility savedEntity = repository.save(facility);
    
    assertEquals("Bloorgate", repository.findById(savedEntity.getId()).getAddress().getCity());
  }
}

Das FacilityRepositiory wird in der setup Methode initialisiert und direkt verwendet. Die bereitgestellten Implementierungen stellen ein konsistentes Verhalten der Repositories da. Erstellt oder entfernt der zu testende Service irgendwelche Entitäten, so kann dies am Ende des Tests im Repository geprüft werden.

Einen kleinen Nachteil birgt das Spring Data Mock Projekt leider noch. Es werden noch keine Repositories unterstützt, die Optionals als Ergebnis liefern. Eine interessante Alternative für exzessive Mockito Tests ist es aber allemal.