cs.Tutorial();

Datenbank mit einer JUnit5 Extension leeren

  • Spring Boot
  • JUnit5
  • Integrationstest

Es gibt viele Gründe um den in einem Test angehäuften Datenstand vor der Ausführung des nachgelagerten Tests aufzuräumen. Vermutlich gibt es auch ebensoviele Ansätze, um dieses Ziel zu erreichen. In diesem Beitrag zeige ich, wie eine Datenbank vor jedem Test mit Hilfe einer JUnit5-Extension auf ihren initialen Stand gebracht werden kann.

DataJpaTest

Wenn die Anwendung die Implementierung eigener Query-Methoden in einem Spring Data JpaRepository erfordert, dann erweist sich das Setup für die entsprechenden Tests als besonders leichtgewichtig. Hierfür liefert das Framework die Annotation @DataJpaTest, die eine speziell auf JPA-Tests zugeschnittene Autokonfiguration des Application-Contexts bereitstellt. Sie sorgt dafür, dass in diesen Tests nur die JPA relevanten Komponenten initialisiert werden und nicht der gesamte Application Context hochgefahren wird. Die in den Tests erzeugten Daten werden nach jedem Lauf automatisch entfernt, sodass der nachfolgende Test mit einer leeren Datenbank starten kann.

@DataJpaTest
public class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@Test
void saveUser() {
userRepository.save(new User("Jane", "Doe"));
assertThat(userRepository.findAll()).hasSize(1);
}
}

SpringBootTest

Ein anderes Bild ergibt sich hingegen, wenn ein Integrationstest implementiert werden muss, in dem ein oder mehrere Spring gemanagte Services verwendet werden.

Die Annotation @DataJpaTest wäre in diesem Fall nicht zielführend, da sie die mit @Service oder @Component annotierten Klassen ignorieren würde. Stattdessen wäre in diesem Fall die Annotation @SpringBootTest die richtige Wahl, um den vollständigen (oder auch eingeschränkten) Application-Context hochzufahren.

Bei solchen Tests könnten jedoch Seiteneffekte entstehen, da alle Tests auf dem gleichen Schema der im Test verwendeten Datenbank arbeiten. So kann z.B. in einem Test ein neuer Benutzer angelegt werden, auf den auch im nachgelagerten Tests zugegriffen werden kann. Dass dadruch Kollisionen entstehen können ist offensichtlich. Die Testergebnisse werden fragil und nicht mehr zuverlässig.

Bei solchen Tests muss sichergestellt werden, dass deren Ausgangslage immer dieselbe ist. Die Datenbank muss demzufolge vor jeder Testausführung auf ihren initialen Zustand zurückgesetzt werden.

Die Datenbank leeren

Was wir brauchen ist also einer Art Aufräumkommando für die Datenbank. Das kann z.B. ein Service sein, der die Datenbank über deleteAll()-Aufrufe auf entsprechenden JpaRepositories leert. Im folgenden Listing wird ein solcher Service implementiert, in dem die Datensätze aus dem UserRepositry entfernt werden. Gäbe es im Projekt andere Jpa-Entities, die mit User in einer @ManyToOne oder @OneToOne Beziehung stünden, so müsste das Leerräumen dieser Repositories entsprechend vorgezogen werden. Bei diesem Beispiel gehen wir jedoch nur von einer Entity aus, so dass keine weiteren deleteAll()-Aufrufe erforderlich sind.

@Service
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class DbCleanUpService {
UserRepository userRepository;
void cleanUp() {
userRepository.deleteAll();
}
}

Das Aufräumen der Datenbank in einen Service auszulagern ist insofern sinvoll, als dass wir dadurch nur eine Stelle anzupassen haben, wenn Änderungen am Schema vorgenommen werden. Würden wir stattdessen die einzelnen Repository-Aufrufe jeweils in das Set-up (eine mit @BeforeEach annotierte Methode im Test) eines jeden Integrationstests auslagern, so hätten wir anschließend einen erhebllich höheren Aufwand all diese Stellen zu ändern.

Stattdessen könnten wir in das Set-up eines jeden Tests den Aufruf des neuen Aufräum-Services einfügen. Besser wäre jedoch die Implementierung einer Extension, die unsere Tests um dieses Verhalten erweitern würde.

JUnit5 Extension

Eine Extension in JUnit5 ist mit einer Rule in JUnit4 vergleichbar - sie kann einen Test um ein gewünschtes Verhalten bzw. Logik erweitern. Abhängig von dem, an welcher Stelle im Lebenszyklus eines Tests eingegriffen werden soll, müssen entsprechende Callback-Interfaces implementiert werden. Möchte man z.B. eine bestimmte Logik unmittelbar vor der Ausführung des Tests aufrufen, so kann das Interface BeforeEachCallback implementiert werden. Eine komplette Liste aller Callbacks samt ihrer Beschreibung kann hier nachgeschlagen werden.

public class MyExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
// ...
}
}

Zugriff auf den ApplicationContext

Interessant wird es, wenn innerhalb einer Extension auf den ApplicationContext zugegriffen werden soll, um sich z.B. den DbCleanUpService geben zu lassen. Dafür kann die Hilfsfunktion getApplicationContext der Klasse SpringExtension genutzt werden. Als Parameter muss ihr der ExtensionContext übergeben werden, der in jeder Callback-Methode zur Verfügung steht. Die geeignete Stelle für den Zugriff auf die von Spring gemanagte Beans, liegt vor der Ausführung aller Testmethoden einer Test-Klasse. Wir können also den BeforeEachCallback verwenden, um unseren Service aus dem ApplicationContext zu holen und ihn in einer Instanzvariable abzulegen. Anschließend kann im BeforeEachCallback auf den DbCeanUpService zugegriffen und die Datenbank über den cleanUp()-Aufruf auf den initialen Stand gebracht werden.

public class DbCleanUp implements BeforeAllCallback, BeforeEachCallback {
private DbCleanUpService cleanUpService;
@Override
public void beforeAll(ExtensionContext context) {
ApplicationContext ac =
SpringExtension.getApplicationContext(context);
cleanUpService = ac.getBean(DbCleanUpService.class);
}
@Override
public void beforeEach(ExtensionContext context) {
cleanUpService.cleanUp();
}
}

Nun steht noch die Anwendung der implementierten Extension aus. Alles was wir brauchen, um einen Test mit dem Aufräumverhalten auszustatten, ist das Annotieren der Test-Klasse mit @ExtendWith(DbCleanUp.class). Fertig, vor jedem Testlauf wird nun die Datenbank geleert - die Tests haben keine Seiteneffekte mehr und können in beliebiger Reihenfolge ausgeführt werden.

@ExtendWith(DbCleanUp.class)
@SpringBootTest
public class IntegrationTestA {
@Autowired
private UserService userService;
@Test
void saveUser() {
userService.saveUser(new UserDTO("John", "Doe"));
assertThat(userService.count()).isEqualTo(1);
}
}

Auf Github habe ich hierzu ein kleines Projekt vorbereitet, in dem der gesamte Code aus diesem Post enthalten ist. Zusätzlich ist dort auch ein weiterer Integrationstest hinterlegt, mit dem die Seiteneffekte des Test-übergreifenden Datenbankzustands nachvollzogen werden können. Beide Integrationstests setzen die hier beschriebene DbCleanUp-Extension ein. Die Annotation @ExtendWith(...) kann vorübergehend entfernt werden, um die hier beschriebenen Seiteneffekte zu sehen.

Zur Blog-Post Übersicht