Automatisches Testen des Modells und Repositories
https://bildung.social/@oerinformatik/
https://oer-informatik.de/sbb04_repository-test
tl/dr; (ca. 11 min Lesezeit): Die zentralen CRUD-Funktionen des Repositories werden getestet. Dazu wird die given-when-then-Methodik verwendet und in jUnit jeweils Testmethoden implementiert. Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt. Weiter geht es dann mit der Erstellung eines Controllers für die Address-Klasse. (Zuletzt geändert am 04.09.2023)
Das Testumfeld: Address
und AddressRepository
Mit dem Modell und Repository haben wir eine Struktur angelegt, die zunächst recht abstrakt erscheint. Da es ohnehin nötig ist, für diese Klassen Tests zu generieren, nutzen wir diese, um etwas vertrauter zu werden mit unseren beiden neuen Klassen.
Wir testen die @Entity
-Klasse Address
nicht allein, sondern bereits integriert mit dem AddressRepository
. Um einen Überblick zu erhalten, welche Methoden wir testen sollten, werfen wir nochmal einen Blick auf das Klassendiagramm des Repositories:

Es sollten folgende Tests generiert werden:
jedes Attribut der Entity sollte einmal gelesen und geschrieben werden:
id
,firstName
,lastName
im CrudRepoistory sollten wir Create/Read/Update/Delete jeweils einmal testen,
create:
save(new Address())
read:
findById()
update:
save(existingAddress)
delete:
delete(existingAddress
)`
Darüber hinaus könnte noch interessant sein:
existsById()
findById()
count()
Die Testklasse erstellen und vorbereiten
Das händische Testen ist wichtig, aber umständlich. Künftig sollte dieser Test automatisch erfolgen. Dazu muss zunächst eine jUnit-Testklasse erzeugt werden. Im Kontextmenü, das beim Klick auf den Repositorynamen erscheint, “Go to Test” auswählen…

…im Aufpoppenden Menü “Generate Test” wählen…

…den Namen übernehmen oder ggf. abändern…

Und es wird am richtigen Ort eine ziemlich leere Klasse erstellt:
Zunächst müssen wir die Integration der Klasse in das Spring-Framework mit @ExtendWith(SpringExtension.class)
ergänzen und die Testmöglichkeiten der JPA mit @DataJpaTest
aktivieren. Dazu müssen die beiden Annotationen an der Klasse ergänzt werden:
Wir nutzen für die Tests den TestEntityManager
(für die erwarteten Ergebnisse) und das AddressRepository
(für die tatsächlichen Ergebnisse) - von beidem werden per @Autowired
automatisch konfigurierte Instanzen verknüpft.
public class AddressRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private AddressRepository addressRepository;
...
}
Darüber hinaus sollten wir in Testklassen vier Methodenrümpfe vorbereiten (einige Frameworks legen diese auch direkt an):
@BeforeAll
setUpClass(){
}
@AfterAll
tearDownClass(){
}
@BeforeEach
setUp(){
}
@AfterEach
tearDown(){
}
Sie dienen zur Durchführung von Aktionen vor und nach allen Tests (@BeforeAll
/ @AfterAll
) bzw. vor und nach jedem einzelnen Test (@BeforeEach
/ @AfterEach
). Derzeit benötigen wir diese Funktionalität nicht, wir können sie daher zunächst leer belassen.
Aufbau der einzelnen Testmethode
Jede Testmethode wird mit @Test
annotiert, damit jUnit sie findet und im Rahmen der Tests ausführt. Sie besteht aus drei Operationen, die explizit in Abschnitten ausgewiesen sein können, oder in einer Operation zusammengefasst werden können:
Vorbedingungen festlegen,
das tatsächliche Ergebnis des Testaufrufs bestimmen,
das erwartetes Ergebnis festlegen, mit dem Ergebnis des Testaufrufs vergleichen und das Testresultat ausgeben.
Die Lesbarkeit von Tests erhöht sich deutlich, wenn man diese drei Operationen in jeder Testmethode klar gegliedert implementiert:
Test-Vorbereitung (Preparation, given
):
Hier wird alles vorbereitet:
- Die Vorbedingungen für den Aufruf der zu testenden Methode werden erfüllt: die nötigen Parameter und die nötigen Instanzen werden erstellt.
Je nach Testumfang kann dieser Abschnitt relativ groß werden. Er wird häufig mit \\given
markiert.
Test-Ausführung (Execution, when
):
Hier findet lediglich der eigentliche Aufruf der getesteten Methode statt. Soweit nötig wird der Rückgabewert ermittelt. An diesem Abschnitt kann man leicht erkennen, was dieser Test überprüft. Dieser Bereich umfasst häufig nur eine Zeile und kann zur Verdeutlichung mit \\when
markiert werden.
Nachweis des Ergebnisses (Verification, then
):
Das erwartete Ergebnis wird hier erzeugt oder festgelegt. Mit Hilfe von Zusicherungsmethoden (z.B. assert*()
) wird dann das erwartete Ergebnis mit dem tatsächlichen Ergebnis der zu testenden Methode verglichen. Es kann eine oder mehrere Zusicherungen abgefragt werden. Wird eine Zusicherung nicht erfüllt, wirft die genutzte Zusicherungsmethode einen AssertionError
- daran merkt jUnit, dass der Test gescheitert ist. (Wer mag, kann das direkt ausprobieren, und mit Hilfe von throw new AssertionError("Jetzt scheitert der Test vorsätzlich!");
den Test einmal probehalber scheitern lassen.) Es ist verbreitet, diesen Abschnitt mit \\then
zu markieren.
Beispielhafter Aufbau
@Test
public void testXY() {
//given
Object testObjekt = new TestKlasse();
Long eingabeparameter = 123;
//when
Object result = testObjekt.operation(eingabeparameter);
//then
Long expResult = 456;
assertThat(result).isEqualTo(expResult);
}
Create
-Test (save(new Address())
):
Um die Erzeugung und das Speichern einer Adresse testen zu können wird zunächst eine Address-Instanz erzeugt…
… durch das Repository gespeichert…
… und mit dem TestEntityManager
überprüft, ob diese Address
-Instanz dieselbe ist, wie die gespeicherte Instanz.
Address expResult = testAddress;
Address result = entityManager.find(Address.class, addressId);
assertThat(result).isEqualTo(expResult);
Bei der Schreibweise der letzten Zeile fällt auf, dass sie wie ein Text zu lesen ist. Man nennt derart verkettete Aufrufe eine FluentAPI. Jeder Methodenaufruf ändert das bestehende Objekt und gibt es selbst als Rückgabewert zurück. Dadurch können weitere verkettete Aufrufe darauf vorgenommen werden. Häufig wird der Import für diese genutzte Bibliothek nicht direkt gefunden. Es müsste also oben, im Import-Bereich ergänzt werden:
Wir nutzen die Klasse TestEntityManager
, um herauszufinden, welcher Eintrag sich im Repository befindet (Aufruf: entityManager.find(Address.class, addressId);
). Die Klasse bietet eine Reihe weiterer Methoden, mit denen wir unsere Persistenzschicht ansprechen können, ohne direkt auf das Repository zuzugreifen. Einige werden wir später noch benötigen, daher hier ein kleiner (in eine UML-Klassendiagramm gewandelter) Auszug aus dem zugehörigen JavaDoc.

Im ganzen könnte der Test z.B. wie im folgenden abgedruckt aussehen. Der Testmethodenname benennt zum einen die getestete Methode (save
), zum anderen, was er macht (createAddress
). Es wurden noch einige Ausgaben angefügt, die natürlich funktional nicht notwendig sind, aber zum Verständnis dienen sollen:
@Test
public void testSave_createAddress() {
// given
System.out.println("Teste das Erzeugen und Speichern von Adresseinträgen");
String firstName = "Hannes";
String lastName = "Stein";
Address testAddress = new Address(firstName, lastName);
System.out.println("Folgendes Objekt wird gespeichert: " + testAddress);
// when
addressRepository.save(testAddress);
long addressId = testAddress.getId();
System.out.println("Vergebene ID: " + addressId);
System.out.println("Danach im RePo vorhanden: " + addressRepository.findAll());
// then
Address expected = testAddress;
Address result = entityManager.find(Address.class, addressId);
assertThat(result).isEqualTo(expected);
}
Falls es Probleme beim automatischen finden der Imports gibt: die Liste müsste insgesamt lauten:
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.*;
Die Tests aufrufen
Wir haben verschiedene Möglichkeiten, die Ausführung dieses Tests zu starten:
- Im Projektexplorer im Abschnitt “Java Projekts” gibt es ein Play-Symbol neben dem Java-Filenamen des Tests:

Die Debugging-Infos sind hier im Fenster “Debugging Konsole” zu lesen (Menü: Anzeigen / Debugging Konsole).
- Wir rufen das entsprechende Maven-Goal auf: führe alles aus bis inklusive der Unittestphase mit allen Tests:
mvn test -f ".\pom.xml"
Es gibt noch viele andere Möglichkeiten. Alle haben gemein, dass das zusammengefasste Testergebnis etwa so auf der Konsole erscheint:
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 29.110 s
[INFO] Finished at: 2022-09-06T08:33:25+02:00
[INFO] ------------------------------------------------------------------------
Ein bisschen ausführlicher ist das Ergebnis in der Darstellung unter Menü: Anzeigen/Testen:

Irgendwo, tief in den Texten des Terminals verborgen, finden sich auch unsere Debugging-Ausgaben (gemischt mit den Debugging-Infos von Hibernate, dem DB-Framework):
Teste das Erzeugen und Speichern von Adresseinträgen
Folgendes Objekt wird gespeichert: Address [city=Berlin, country=Deutschland, firstName=Hannes, id=null, lastName=Stein, postalCode=10435, street=Musterstraße, streetAdditional=123]
Hibernate: call next value for hibernate_sequence
Vergebene ID: 2
Hibernate: insert into adressen (city, country, first_name, last_name, postal_code, street, street_additional, id) values (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select address0_.id as id1_0_, address0_.city as city2_0_, address0_.country as country3_0_, address0_.first_name as first_na4_0_, address0_.last_name as last_nam5_0_, address0_.postal_code as postal_c6_0_, address0_.street as street7_0_, address0_.street_additional as street_a8_0_ from adressen address0_
Danach im RePo vorhanden: [Address [city=Berlin, country=Deutschland, firstName=Hannes, id=2, lastName=Stein, postalCode=10435, street=Musterstraße, streetAdditional=123]]
Wer detailliertere Infos zum Testergebnis benötigt, der findet die erzeugten Protokolle u.a. als XML
-Dateien im Projektverzeichnis unter target/surefire-reports/
. Stürzen wir uns auf den nächsten Test:
Read
-Test (findById()
)
Um den Lesevorgang zu testen benutzen wir die Methode findById()
des Repositories. Diese liefert ein Objekt der Klasse Optional<Address>
. Optional
ist generisch (also für alle Klassen nutzbar), wird aber hier für die Klasse Address
parametrisiert. Optional
liefert den Wert des enthaltenen Address
-Objekts zurück - oder eben NULL
.

Um diesem Optional das Address
-Objekt zu entlocken, müssen wir die get()
Methode aufrufen. Darüber hinaus kann beim Testen die Methode isPresent()
interessant sein, die prüft, ob ein Objekt existiert.
Ich gehe wieder im Dreischritt given-when-then
vor:
given
: Es muss zunächst eine Address-Instanz erzeugt werden…
… durch den entityManager
am Respository vorbei gespeichert werden …
when
: Wir wollen es mit dem Repository auslesen:
then
: …und prüfen, ob das mit dem Repository gelesen Objekt dem entspricht, was wir zu lesen glauben:
Address expResult = testAddress;
Address result = optAddress.get();
assertThat(result).isEqualTo(expResult);
assertThat(result.getFirstName()).isEqualTo("Lars");
Der Import für Optional
muss dann oben noch ergänzt werden:
Beispielhaft - und wieder mit Ausgaben erweitert - könnte das im Ganzen so aussehen:
@Test
public void testfindById_readAddress() {
// given
System.out.println("Teste das Lesen von Adresseinträgen");
String firstName = "Lars";
String lastName = "Liesmich";
Address testAddress = new Address(firstName, lastName);
long addressId = entityManager.persistAndGetId(testAddress, Long.class);
entityManager.flush();
System.out.println("Folgendes Objekt wird gespeichert: " + testAddress);
System.out.println("Vergebene ID: " + addressId);
System.out.println("Danach im RePo vorhanden: " + addressRepository.findAll());
// when
Optional<Address> optAddress = addressRepository.findById(addressId);
// then
Address expResult = testAddress;
Address result = optAddress.get();
assertThat(result).isEqualTo(expResult);
assertThat(result.getFirstName()).isEqualTo("Lars");
}
Läuft der Test? Im Terminal mvn test -f ".\pom.xml"
ausführen und die Ergebnisse unter Menü: Anzeigen/Testen bewundern!
Update
-Test (save(Address address)
)
Hierbei wird die gleiche Methode getestet, wie bei dem create
-Test: die save()
-Methode des Repositories. Einziger Unterschied ist, dass das Address
-Objekt diesmal bereits im Repository enthalten ist und nur aktualisiert werden soll. Der Ablauf ähnelt demnach auch dem des create
-Tests:
- Vorbedingungen (
given
): Es wird eine Address-Instanz erzeugt:
… durch den EntityManager
gespeichert und die id
ausgelesen…
- Ausführung (
when
): Die Attribute werden verändert…
und über das Repository gespeichert (geupdatet)…
- Abgleich (
then
): Es wird geprüft, ob das erwartete Ergebnis dem gespeicherten entspricht:
Address expectedAddress = testAddress;
Address resultAddress = entityManager.find(Address.class, addressId);
assertThat(resultAddress).isEqualTo(expectedAddress);
Beispielhaft könnte das so aussehen:
@Test
public void testSave_updateAddress() {
// given
System.out.println("Teste das Aktualisieren von Adresseinträgen");
String firstName = "Martin";
String lastName = "Müller";
Address testAddress = new Address(firstName, lastName);
System.out.println("Folgendes Objekt wird gespeichert: " + testAddress);
long addressId = entityManager.persistAndGetId(testAddress, Long.class);
entityManager.flush();
System.out.println("Vergebene ID: " + addressId);
System.out.println("Danach im RePo vorhanden: " + addressRepository.findAll());
// when
testAddress.setFirstName("Stefan");
testAddress.setLastName("Beispiel");
addressRepository.save(testAddress);
// then
Address expectedAddress = testAddress;
Address resultAddress = entityManager.find(Address.class, addressId);
assertThat(resultAddress).isEqualTo(expectedAddress);
}
Auch den Test testen wir erst einmal im Terminal: mvn test -f ".\pom.xml"
. Alles ok?
Delete
-Test
Auch hier kann ähnlich vorgegangen werden:
- Vorbedingungen (
given
): Es wird eineAddress
-Instanz erzeugt:
… durch den entityManager
gespeichert…
- Ausführung (
when
): Wir löschen die betreffendeAddress
-Instanz mit Hilfe der Repository-Methode…
- Abgleich (
then
): Abschließend prüfen wir, ob im Repository wirklich kein Objekt mehr mit derid
verfügbar ist:
isNull()
wäre natürlich auch erfüllt, wenn bereits das Speichern gescheitert wäre. Diesen Fall fangen wir jedoch mit dem vorherigen Testfall (für save()
) ab. Um dennoch sicherzugehen, dass der Eintrag vorher vorhanden und erst dann gelöscht wurde fügen wir eine weitere Zusicherung ein, die diesen Fall ausschließt:
Im given
-Teil vor dem Löschen kopieren wir das unter der ID gespeicherte Objekt:
… und vergleichen im then
-Teil, ob das aktuell unter der ID gespeicherte Objekt (also nichts mehr) davon abweicht:
Beispielhaft könnte das so aussehen:
@Test
public void testDeleteAddress() {
// given
System.out.println("Teste das Löschen von vorhandenen Adresseinträgen");
entityManager.persist(new Address("Dagmar", "Düsentrieb"));
Address testAddress = new Address("Lisa", "Löschmich");
long addressId = entityManager.persistAndGetId(testAddress, Long.class);
entityManager.flush();
System.out.println("Folgendes Objekt wird gespeichert: " + testAddress);
System.out.println("Vergebene ID: " + addressId);
System.out.println("Danach im RePo vorhanden: " + addressRepository.findAll());
final Address addressInRepoBefore = entityManager.find(Address.class, addressId);
// when
addressRepository.delete(testAddress);
System.out.println("Nach löschen vorhanden: " + addressRepository.findAll());
// then
assertThat(entityManager.find(Address.class, addressId)).isNull();
assertThat(entityManager.find(Address.class, addressId)).isNotEqualTo(addressInRepoBefore);
}
Überblick über die Testklasse
Vier Tests sind abgeschlossen, wir schauen uns noch einmal nach Ausführen des Maven-Test-Goals (mvn test -f ".\pom.xml"
) die Ergebnisse unter Menü: Anzeigen/Testen an:

Das sieht doch ganz gut aus.
Im Ganzen sieht die Klasse mit vier implementierten und vier nicht implementierten Methoden etwa so aus:
Nächste Schritte
Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt.
Modell und das Repository können noch nicht per Webrequest angesprochen werden. Um das zu realisieren muss als nächstes ein Controller die HTTP-Endpunkt für Create, Read, Update und Delete anbieten: POST, GET, PUT, DELETE.
Weiter geht es also mit der Erstellung eines Controllers für die Address-Klasse.
Hinweis zur Nachnutzung als Open Educational Resource (OER)
Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY-SA 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Automatisches Testen des Modells und Repositories” von Hannes Stein, Lizenz: CC BY-SA 4.0. Der Artikel wurde unter https://oer-informatik.de/sbb04_repository-test veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Stand: 04.09.2023.
[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]