CodeGym /Kurse /Spring Test /Erster ArticleRepositoryDataJpaTest

Erster ArticleRepositoryDataJpaTest

Spring Test
Level 14, Lektion 4
Verfügbar

1. Startpaket Tests

Wenn Sie jemals versucht haben, „alle Tests auf einmal zu schreiben“, kennen Sie diese Handlung: am Anfang Begeisterung, nach einer Stunde Traurigkeit, nach zwei Kaffee, nach drei philosophische Fragen an das Leben und Hibernate. Daher ist es besser, den data-layer auf die gleiche Weise zu starten, wie wir den web-layer gemacht haben: mit kleinen, verständlichen, überprüfbaren Schritten. Das erste Paket von Repository-Tests sollte Ihnen minimale Sicherheit geben, dass die Datenbank Ihre Entität Article „sieht“ und dass die grundlegenden Lese-/Schreib-Szenarien funktionieren, ohne die Testklasse in einen tausendseitigen Roman zu verwandeln.

Zu diesem Zeitpunkt haben wir bereits zwei Stützen: Es ist klar, welche Vereinbarungen ArticleRepository tatsächlich einen eigenen Test wert sind, und es ist klar, wie man Daten mit TestEntityManager vorbereitet, ohne das Setup in einen zweiten Repository-Test zu verwandeln. Nun kann man aus diesen Stücken den ersten zusammenhängenden ArticleRepositoryDataJpaTest zusammenbauen – eine kleine Klasse, die anfängliches Vertrauen in die Speicherschicht gibt und nicht mit unnötigen Details rauscht.

In ContentHub hat ein Artikel sinnvolle Felder, ohne die die Domäne nicht existiert: slug, status, authorUsername (und natürlich die Verknüpfung mit der Kategorie). Und es gibt eine einfache Tatsache, die wir uns zu Beginn beweisen müssen: „Ich kann einen Artikel speichern, ich kann ihn zurücklesen, und mein Repository lügt zumindest in grundlegenden Dingen nicht.“ Ja, das klingt minimal, aber es ist die Minimalität, die Nerven spart.

Ein weiterer wichtiger Grund, das Startpaket klein zu halten, ist die Geschwindigkeit. @DataJpaTest ist schneller als der volle Kontext, aber immer noch teurer als ein Unit-Test. Wenn Sie zu Beginn 25 Tests in einer Klasse schreiben (dazu noch mit riesigem Setup), erhalten Sie eine langsame Suite und das Gefühl, dass „Tests eine Qual sind“. Wir brauchen das Gegenteil: schnelles Feedback und verständliche Fehlschläge.

2. Gerüst ArticleRepositoryDataJpaTest

Jeder gute Test beginnt nicht mit heroischen Assertions, sondern mit einem richtigen „Rahmen“. Für data-layer ist der Rahmen @DataJpaTest und Disziplin: Test im Repository-Paket, nicht irgendwo „in Tests allgemein“. Die Idee ist einfach: Wenn Sie das Projekt in einem Monat wieder öffnen, sollen Sie sofort verstehen, dass dies ein data slice Test ist, der nichts mit Controllern, JSON und Statuscodes zu tun hat.


package com.example.contenthub.repository;

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;

@DataJpaTest
class ArticleRepositoryDataJpaTest {

    @Autowired
    ArticleRepository articleRepository; // Repository, dessen Verhalten wir in den Tests überprüfen

    @Autowired
    TestEntityManager entityManager; // Bequeme Möglichkeit, Daten vorzubereiten (persist/flush) ohne separates Repository
}

Beachten Sie den psychologischen Effekt: Wenn Sie @DataJpaTest sehen, sollte Ihr Gehirn sofort in den Modus „Ich teste das Persistence-Verhalten“ umschalten. Wenn in dieser Klasse plötzlich MockMvc oder Services auftauchen, ist das fast immer ein Zeichen, dass Sie Schichten vermischt haben.

Unten ist ein Beispiel, wie ein Repository normalerweise aussieht, für das wir diesen Test schreiben. Uns ist hier eines wichtig: die Lesemethode nach slug, da dies ein domänensignifikanter Schlüssel ist.


import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ArticleRepository extends JpaRepository<Article, Long> {

    // Domänensignifikanter Weg, einen Artikel zu lesen: Slug wird in URL und Editorszenarien verwendet
    Optional<Article> findBySlug(String slug);
}

3. Minimale Fixture für Article

Entitäten sind fast immer so aufgebaut, dass „einfach ein Objekt erstellen“ nicht funktioniert: Man muss Pflichtfelder, Verknüpfungen, manchmal Daten füllen. Und genau hier fangen die meisten Anfänger-Tester von JPA den ersten „magischen“ Fehler ein: Der Test schlägt fehl, aber nicht weil das Repository schlecht ist, sondern weil wir einen ungültigen Artikel erstellt haben. Das ist ziemlich ärgerlich: Sie wollten save testen und haben stattdessen Ihre Unaufmerksamkeit getestet.

In ContentHub ist ein Artikel mit einer Kategorie verknüpft, und eine Kategorie ist nicht nur ein String, sondern eine separate Entität. Daher ist der ehrlichste und verständlichste Ansatz für das Startpaket, kleine Helper-Methoden im Test zu erstellen: Eine erstellt und speichert eine Kategorie, die zweite erstellt einen Entwurf eines Artikels mit einem minimalen Satz an Feldern. Wir bauen hier keine „Fabrik für Testdaten der Marke Werk“, aber wir kopieren auch nicht 12 Zeilen in jeden Test.


import com.example.contenthub.entity.Category;

// Helper innerhalb der Testklasse: Erstellen und Speichern einer Category, damit Article gültig ist
private Category persistedCategory(String code) {
    Category c = new Category();
    c.setCode(code); // Domänencode der Kategorie (z.B. "java", "spring", "db")
    c.setName(code.toUpperCase()); // Einfach menschenlesbarer Name für die Vollständigkeit der Entität
    return entityManager.persist(c); // persist -> erhalten ein managed-Objekt und die ausgefüllte id (falls generiert)
}

import com.example.contenthub.entity.Article;
import com.example.contenthub.entity.ArticleStatus;

// Helper innerhalb der Testklasse: Erstellen eines minimal gültigen Artikels (sonst schlagen Tests grundlos fehl)
private Article draftArticle(String slug, Category category) {
    Article a = new Article();
    a.setTitle("Spring Data basics"); // Kein Schlüsselfeld des Tests, aber oft erforderlich/nützlich für die Gültigkeit
    a.setSlug(slug); // Schlüsselfeld der Domäne: Lesen darüber via findBySlug(...)
    a.setSummary("Short summary"); // Minimal nicht leerer Wert
    a.setBody("Body text"); // Minimal nicht leerer Wert
    a.setStatus(ArticleStatus.DRAFT); // Status explizit festlegen, damit der Test nicht von Voreinstellungen abhängt
    a.setAuthorUsername("alice"); // Autor ist wichtig für Domänenszenarien
    a.setCategory(category); // Beziehung erforderlich: Artikel ohne Kategorie ist oft ungültig
    return a;
}

Ja, das ist kein idealer Domain-Builder, und ja – in einem echten Projekt könnten Sie Fabriken oder statische Methoden haben. Aber für das erste Paket von Repository-Tests ist dieser Ansatz gut, weil er transparent ist: Sie sehen buchstäblich, welcher Artikel in die Datenbank gelangt.

4. Test Nr.1: Speichern und Basisstatus

Der erste Test im Repository-Paket sollte maximal „langweilig“ und maximal nützlich sein. Wir prüfen hier keine Business-Orchestrierung und bauen keinen Lebenszyklus des Artikels – wir beweisen lediglich, dass save die Entität tatsächlich so speichert, wie die Domäne es erwartet. Die grundlegendsten Erwartungen: Der gespeicherte Artikel hat eine id bekommen, der slug wurde gespeichert, und der Status entspricht dem Entwurf.

Beachten Sie die Struktur AAA (Arrange–Act–Assert). Im Arrange bereiten wir die Kategorie vor (sonst wäre der Artikel ein „Halbfertigprodukt“), im Act rufen wir save auf, im Assert prüfen wir ein paar sinnvolle Dinge – nicht 20 Felder, sondern genau das, was Vertrauen schafft, dass der Eintrag „nicht seltsam“ ist.


import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;

@Test
void savesDraftArticle() {
    // Arrange: obligatorische verknüpfte Entität
    Category java = persistedCategory("java");

    // Act: Entwurf über Repository speichern
    Article saved = articleRepository.save(draftArticle("spring-data-basics", java));

    // Assert: Überprüfung mehrerer "Anker"-Fakten, die wirklich Sicherheit geben
    assertThat(saved.getId()).isNotNull(); // id sollte nach dem Speichern vorhanden sein
    assertThat(saved.getSlug()).isEqualTo("spring-data-basics"); // slug sollte nicht "verloren" gehen
    assertThat(saved.getStatus()).isEqualTo(ArticleStatus.DRAFT); // Status entspricht der Fixture-Erwartung
}

Dies ist eine anfängliche Sanity-Prüfung innerhalb des aktuellen Persistence Context. Sie zeigt, dass das Repository und das JPA-Wiring lebendig und konsistent sind, beweist aber noch nicht das härteste Szenario „gespeichert, Änderungen in die DB geschoben, neu gelesen“. Für eine solche Prüfung muss man genauer mit flush/clear und dem Zustand des Persistence Context arbeiten.

Hier kann man leicht ins Extrem verfallen und alles prüfen: createdAt, updatedAt, version, und „lass den Titel genau übereinstimmen“. Aber zu Beginn ist es besser, den Test kurz zu halten: Wenn save gar nicht funktioniert, werden Sie das auch so sehen. Wenn es funktioniert, reichen ein paar Anker-Prüfungen, damit der Test nützlich und nicht laut ist.

5. Test Nr.2: Lesen nach id

Der nächste Schritt – „read-after-write“ innerhalb eines Data-Slice-Tests nachweisen. Dies ist kein „Integrationstest des gesamten Systems“, sondern lediglich eine grundlegende Prüfung, ob die Lesemethode (findById) in der Lage ist, die gespeicherte Entität zurückzugeben. Und ja, findById ist eine Standard-CRUD-Methode, aber sie ist so fundamental, dass das Startpaket einmal das Recht hat, sie zu fixieren, insbesondere wenn Sie nebenbei prüfen, dass domänenwichtige Felder zurückgegeben werden.

Hier prüfen wir zumindest authorUsername. Warum gerade dieses? Weil der Autor ein Schlüsselfeld der ContentHub-Domäne ist: Editoren arbeiten mit „ihren“ Artikeln, und dieses Feld wird in realen Szenarien verwendet.


import java.util.Optional;
import org.junit.jupiter.api.Test;

@Test
void readsSavedArticleById() {
    // Arrange: gültigen Artikel speichern
    Category java = persistedCategory("java");
    Article saved = articleRepository.save(draftArticle("spring-data-basics", java));

    // Act: Lesen über technischen Schlüssel (id) mit Standard-CRUD-Methode
    Optional<Article> reloaded = articleRepository.findById(saved.getId());

    // Assert: Entität gefunden und enthält domänenwichtiges Feld
    assertThat(reloaded).isPresent();
    assertThat(reloaded.orElseThrow().getAuthorUsername()).isEqualTo("alice");
}

Es ist wichtig zu verstehen, dass Sie sich innerhalb von @DataJpaTest in einer Transaktion befinden. Das ist praktisch, weil alles „innerhalb des Tests“ geschieht und keine manuelle Datenbereinigung erfordert. Aber das bedeutet auch, dass der Persistence Context aktiv mitarbeitet. Für das Startpaket reicht eine solche grundlegende Read-After-Write-Prüfung: Sie zeigt schnell, dass das Repository die gespeicherte Entität zurückgeben kann und keine domänenwichtigen Felder verliert. Ein härterer Beweis des physischen Round-Trips zur Datenbank ist eine separate Aufgabe.

6. Test Nr.3: slug nicht gefunden – leer

Wenn Sie nur den Happy Path schreiben, werden Tests wie eine Neujahrskarte: schön, aber die Realität wird trotzdem mit ihren Problemen kommen. Daher ist der dritte Test im Startpaket unbedingt ein negative case. Wir legen den Vertrag fest: Wenn kein Artikel mit diesem slug existiert, gibt das Repository Optional.empty() zurück. Das ist gleichzeitig nützlich und sehr billig.

Das Schönste daran ist, dass der Test überhaupt kein Setup benötigt. Wir müssen nichts in die Datenbank speichern, um das Fehlen von Daten zu prüfen: Die leere Datenbank zu Beginn des Tests ist ein normaler Zustand dank rollback by default (und weil jeder Test unabhängig ist).


import org.junit.jupiter.api.Test;

@Test
void returnsEmptyForUnknownSlug() {
    // Assert: Fehlen von Daten ist auch ein Vertrag. Hier absichtlich kein Arrange/Act mit Speicherung.
    assertThat(articleRepository.findBySlug("unknown-slug")).isEmpty();
}

Dieser Test sieht lächerlich kurz aus. Aber genau solche Tests retten oft vor Regressionen der Art „wir haben versehentlich die Query geändert und jetzt gibt sie etwas Seltsames statt empty zurück“. Auf Repository-Ebene ist „leer“ ein wichtiges Ergebnis, nicht „na ja, da wird schon null sein“.

Wiederholter Setup: Helper und Konstanten

Irgendwann werden Sie feststellen, dass Sie selbst für drei Tests denselben slug, denselben Autor und dieselbe Kategorieerstellung wiederholen. Und dann greift die Hand nach einem universellen DSL der Art „Schreiben wir doch ein aDraft()...“. Das mag eine gute Idee sein – aber nicht heute. Heute ist es wichtig, die Tests einfach zu halten, weil wir noch lernen, JPA-Tests als Szenarien zu lesen.

Es reichen kleine Helper direkt in der Testklasse und ein paar Konstanten. Dieser Kompromiss funktioniert gut: Die Tests werden kürzer, aber Sie sehen immer noch, welche Daten verwendet werden. Zum Beispiel kann man den Autor und den Basisslug in Konstanten auslagern, damit sie nicht als zufällige Strings durch die Tests „schwimmen“.


private static final String AUTHOR = "alice";
private static final String SLUG = "spring-data-basics";

private Article draftArticle(String slug, Category category) {
    Article a = new Article();
    a.setSlug(slug); // slug als Parameter belassen, damit Tests Szenarien variieren können
    a.setAuthorUsername(AUTHOR); // Konstante: Autor im gesamten Startpaket gleich
    a.setStatus(ArticleStatus.DRAFT); // wichtig, Status explizit festlegen
    a.setCategory(category); // ohne Kategorie kann die Entität ungültig sein
    return a;
}

Ja, hier haben wir einige Felder zugunsten der Kürze des Beispiels entfernt. In Ihrem realen Projekt füllen Sie genau die Felder aus, die für das korrekte Speichern der Entität erforderlich sind. Wichtig ist: Helper sollen dem Test helfen, nicht den Test in einen „Aufruf des Testdaten-Frameworks“ verwandeln.

7. Karte der Starttests

Damit Repository-Tests nicht zu einer Sammlung von „weil ich kann“ werden, ist es nützlich, eine kleine Matrix im Kopf zu behalten: was wir überprüfen und welches Risiko wir damit abdecken. Das ist besonders wichtig für Anfänger, weil sonst die Versuchung besteht, alles möglichst zu testen: „Testen wir doch count, exists, findAll…“ – und schon ist es Abend und der Nutzen fraglich.

Unten – eine kompakte Tabelle für unser erstes Paket. Sie erhebt keinen Anspruch auf Universalität, aber sie hilft, die Logik zu erkennen: Jeder Test beweist ein Versprechen des Repositories.

Test Was beweisen wir Welches Risiko decken wir ab Minimaler Setup
savesDraftArticle Entität wird gespeichert und erhält id Schreiben „funktioniert gar nicht“, falscher Basisstatus Kategorie + Entwurf
readsSavedArticleById Lesen nach id gibt den Artikel zurück Repository gibt die Entität nach dem Speichern nicht zurück Kategorie + Speicherung
returnsEmptyForUnknownSlug Lesen nach slug gibt korrekt leer zurück falscher Lesevertrag, „seltsame“ Ergebnisse anstelle von empty nicht benötigt

Wenn man diese Matrix im Kopf behält, wird es einfacher, nicht in „wir testen das Framework“ abzurutschen. Wir testen das Verhalten unserer Speicherschicht im Kontext der ContentHub-Domäne.

Um die Struktur des Szenarios zu festigen, kann man sich einen data-test als einfache Linie vorstellen: Zustand vorbereiten → Repository-Methode aufrufen → Ergebnis überprüfen. Das klingt fast kindisch, aber genau diese „Kindlichkeit“ macht Tests lesbar.

flowchart LR
    A["Arrange: persist Category / Artikel vorbereiten"] --> B["Act: Methode von ArticleRepository aufrufen"]
    B --> C["Assert: Optional / id / slug / status prüfen"]

8. Typische Fehler in @DataJpaTest

Fehler Nr.1: Services und Controller in @DataJpaTest schleppen.
Wenn Sie die ersten data-layer Tests schreiben, lebt Ihr Gehirn normalerweise noch im MVC-Test- oder Unit-Test-Modus, daher sind die Fehler sehr charakteristisch. Oft wird erwartet, dass in @DataJpaTest Services oder Controller verfügbar sind, und man beginnt, sie in den Kontext zu ziehen, „einfach damit es funktioniert“. Am Ende verlieren Sie die Idee des Slice-Tests und erhalten einen teuren, auseinanderfallenden Kontext, der schwerer zu warten und zu debuggen ist.

Fehler Nr.2: Ungültige Fixture (z.B. Artikel ohne Kategorie).
Eine andere häufige Geschichte: Einen Article ohne Kategorie (oder ohne Pflichtfelder) erstellen und dann lange auf den Testfehlschlag als „Repository-Problem“ starren. In der Praxis ist es ein Fixture-Problem: Sie haben ein Objekt gebaut, das nicht korrekt gespeichert werden kann. Für das Startpaket ist es besser, langweilig und ehrlich zu sein: eine minimal gültige Entität zu erstellen, wenn auch manuell.

Fehler Nr.3: Ein Test „über das gesamte Leben des Artikels“.
Eine weitere Falle: der Versuch, „alles auf einmal“ in einem Test zu prüfen. Zum Beispiel einen Artikel speichern, nach id lesen, nach slug lesen, aktualisieren, löschen und count prüfen. Ein solcher Test sieht sparsam aus, ist aber sehr schlecht diagnostizierbar: Er fällt durch, und man weiß nicht genau, was kaputt ist. Viel nützlicher sind drei kurze Tests mit jeweils einem Versprechen als ein langer Test „über das gesamte Leben des Artikels“.

Fehler Nr.4: Nur id prüfen und nichts anderes.
Manchmal trifft man auch das andere Extrem: Nur id prüfen und nichts anderes. Ja, id ist wichtig, aber wenn Sie kein einziges sinnvolles Feld prüfen, erhalten Sie einen Test, der selbst bei unangenehmen Regressionen grün bleibt (z.B. wenn der Standardstatus des Artikels unerwartet geändert wurde). In Repository-Tests liegt der Wert normalerweise in sinnvollen Ankern wie slug, status, authorUsername und nicht nur in der technischen Tatsache, dass der Eintrag existiert.

Fehler Nr.5: Das Repository im Repository-Test mocken.
Und der letzte klassische Fehler: das Repository im Repository-Test mocken. Das klingt wie ein Witz, aber manchmal wird tatsächlich @MockBean ArticleRepository articleRepository gemacht und dann gewundert, warum „alles grün ist“. Natürlich ist alles grün: Sie haben gerade das aus dem Test entfernt, was er testen soll. @DataJpaTest ist ohnehin maximal schmal und schnell für seine Schicht – hier sind Mocks normalerweise nicht nötig und sogar schädlich.

1
Aufgabe
Spring Test, Level 14, Lektion 4
Gesperrt
Erster `ArticleRepositoryDataJpaTest`
Erster `ArticleRepositoryDataJpaTest`
1
Aufgabe
Spring Test, Level 14, Lektion 4
Gesperrt
Startpaket f?r `CatalogItemRepository`
Startpaket f?r `CatalogItemRepository`
1
Umfrage/Quiz
JPA-Tests, Level 14, Lektion 4
Nicht verfügbar
JPA-Tests
Tests von Repositories und Daten
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION