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.
@DataJpaTestpublic class UserRepositoryTest {@AutowiredUserRepository userRepository;@Testvoid 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 {@Overridepublic 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;@Overridepublic void beforeAll(ExtensionContext context) {ApplicationContext ac =SpringExtension.getApplicationContext(context);cleanUpService = ac.getBean(DbCleanUpService.class);}@Overridepublic 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)@SpringBootTestpublic class IntegrationTestA {@Autowiredprivate UserService userService;@Testvoid 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.
Links
- Github-Projekt DbCleanUp Extension
- DataJpaTest Dokumentation
- SpringBootTest Dokumentation
- JUnit5 Extensions
- JUnit5 Extension Lifecycle Callbacks