Übungsaufgaben zu System- und Abnahmetest: Die Mitfahrgesuche abnehmen

https://bildung.social/@oerinformatik/

https://oer-informatik.de/spring-boot-systemtest-abnahmetest-uebungsaufgaben

Systemtest, Abnahmetest, RestTestClient, SpringBootTest, Abnahmekriterien, Abnahmeprotokoll, CORS, TestBean, Übungsaufgaben

tl/dr; (ca. 34 min Lese- und Bearbeitungszeit): Übungen zu System- und Abnahmetest im Mitfahr-Backend: Du ordnest Prüfungen den Testebenen zu, liest einen Testbericht und triffst eine Abnahmeentscheidung, schreibst Abnahmetests mit RestTestClient für das Einstellen und Zurückziehen von Mitfahrgesuchen, findest heraus, warum ein Test nach zwei Wochen rot wird, und gibst ein zweites Frontend per CORS frei [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über ein REST-Backend mit Spring Boot: Überblick und Fahrplan / 01 Anforderungen und API-Vertrag / 02 Projekt anlegen / 03 Codegenerierung / 04 Fachlogik und Komponententest / 05 Build-Qualität und CI / 06 Persistenz mit JPA / 07 Repository-Integrationstest / 08 Service und Dependency Injection / 09 REST-API und Fehlerbehandlung / 10 Controller-Integrationstest / 11 System- und Abnahmetest / Übungsaufgaben zu 11

Diese Übungsaufgaben gehören zum Artikel Systemtest und Abnahmetest: Die ganze Anwendung über HTTP prüfen mit RestTestClient und setzen den Übungsstrang zu den Mitfahrgesuchen fort. Ausgangspunkt ist das Projekt im Ordner src/11-systemtest-abnahmetest des Repositorys, ergänzt um die Klassen aus den bisherigen Übungen: Zeitfensterregel, Gesuchstatus, die Entity Gesuch, GesuchRepository, NeuesGesuch, GesuchService, GesuchMapper und GesuchController mit dem erweiterten Vertrag (POST /v1/gesuche, GET und PATCH /v1/gesuche/{gesuchId}). Die Testklassen erben wie im Artikel von SystemtestBasis; dort ist es immer Dienstag, der 20.10.2026, 06:00 Uhr.

Für die Gesuche gibt es eine neue User Story:

US-09 Mitfahrt suchen: Als Mitfahrende*r möchte ich ein Gesuch einstellen, damit Fahrer*innen sehen, dass ich eine Mitfahrt suche.

  • AK-09.1: Angenommen, Oksana (Nutzerin 3) sucht für morgen zwischen 06:45 und 07:15 Uhr eine Fahrt ab Haltepunkt 1 zur Schule. Wenn sie das Gesuch einstellt, dann ist es gespeichert, abrufbar und hat den Status „offen“.
  • AK-09.2: Wenn das Zeitfenster rückwärts läuft oder länger als 90 Minuten ist, dann wird das Gesuch abgelehnt.
  • AK-09.3: Wenn das Datum vor dem heutigen Tag liegt, dann wird das Gesuch abgelehnt.
  • AK-09.4: Angenommen, ein Gesuch ist offen. Wenn Oksana es zurückzieht, dann hat es den Status „zurückgezogen“; ein zweites Zurückziehen wird abgelehnt.
Fahrplan der Artikelreihe im V-Modell; auf den Ebenen Systemtest und Abnahmetest ist Artikel 11 hervorgehoben (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1 und Aufgabe 1.2. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1 und Aufgabe 3.2) keine Probleme bereiten. Notiere Deine Antworten zuerst vollständig, bevor Du die Beispiellösung aufklappst. Code und Tests kannst Du anschließend im Projekt aus dem Repository mit dem Build ausführen; andere fachlich richtige Lösungen sind ebenso richtig.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „System- und Abnahmetest: Die
Mitfahrgesuche abnehmen“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/spring-boot-systemtest-abnahmetest
- https://oer-informatik.de/spring-boot-controller-integrationstest
- https://oer-informatik.de/spring-boot-anforderungen-api-entwurf
- Stilbeispiel: https://oer-informatik.de/spring-boot-systemtest-abnahmetest-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- Systemtest
- Abnahmetest
- RestTestClient
- SpringBootTest
- Abnahmekriterien
- Abnahmeprotokoll
- CORS
- TestBean

Kompetenzstufe: Basiskompetenz (alternativ: Kernkompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Welche Testebene passt? (Basiskompetenz)

Ordne jede Prüfung der Testebene zu, auf der sie im Projekt am besten aufgehoben ist: Komponententest, Integrationstest (Persistenz oder Web-Schicht), Systemtest oder Abnahmetest. (6 Punkte)

Prüfungen rund um die Gesuche
Nr. Prüfung Testebene
1 Die Zeitfensterregel lehnt 06:00 bis 07:31 Uhr ab und erlaubt 06:00 bis 07:30 Uhr.
2 GesuchRepository.save scheitert, wenn der Nutzer 13 nicht existiert (Fremdschlüssel).
3 Die Antwort auf POST /v1/gesuche passt mit Status, Content-Type und Body zum Schema Gesuch in openapi.yaml.
4 Oksana kann ein Gesuch für morgen einstellen und danach abrufen (AK-09.1).
5 Der GesuchController antwortet mit 400, wenn zeitVon „6:45“ lautet, und ruft den Service nicht auf.
6 Ein Frontend unter http://localhost:5173 darf POST /v1/gesuche aus dem Browser aufrufen.

Je Zeile 1 Punkt:

Zuordnung der Prüfungen zu Testebenen
Nr. Testebene Begründung
1 Komponententest reine Fachregel mit Grenzwerten, ohne Spring
2 Integrationstest (Persistenz) braucht Repository, Hibernate und Datenbank (@DataJpaTest)
3 Systemtest prüft die laufende Anwendung gegen den Systementwurf, den Vertrag
4 Abnahmetest prüft ein Abnahmekriterium der User Story
5 Integrationstest (Web-Schicht) Bean Validation und verifyNoInteractions auf den Service-Mock (@WebMvcTest)
6 Systemtest CORS-Header kommen von der ganzen Anwendung über echtes HTTP (CorsSystemTest)

Zeile 6 lässt sich auch im Abnahmetest begründen, wenn eine User Story den Browserzugriff verlangt; im Projekt steht CORS in keiner Story, sondern ist eine technische Festlegung.

Aufgabe 1.2: Einen Testbericht lesen und entscheiden (Basiskompetenz)

Nach einer Änderung an der Zeitfensterregel meldet der Lauf von systemTest für die neue Testklasse GesuchAbnahmeTest:

Abnahmetest US-09 Mitfahrt suchen > AK-09.2 Rückwärts laufendes oder zu langes Zeitfenster wird abgelehnt FAILED
    java.lang.AssertionError at GesuchAbnahmeTest.java:56

4 tests completed, 1 failed

Im Testbericht steht zu diesem Test Status expected:<422 UNPROCESSABLE_CONTENT> but was:<201 CREATED>; Zeile 56 gehört zur zweiten Anfrage des Tests, einem Gesuch von 06:00 bis 07:45 Uhr.

  1. Gib an, welche Abnahmekriterien erfüllt sind und welches nicht. (1 Punkt)

  2. Beschreibe in einem Satz, was die Anwendung falsch macht. (1 Punkt)

  3. Formuliere für das Abnahmeprotokoll die Zeilen „Ergebnis“, „Mängel“ und „Entscheidung“. Begründe die Entscheidung. (3 Punkte)

  1. AK-09.1, AK-09.3 und AK-09.4 sind erfüllt, AK-09.2 nicht (1 Punkt).

  2. Ein Zeitfenster von 105 Minuten (06:00 bis 07:45 Uhr) wird angenommen, obwohl höchstens 90 Minuten erlaubt sind; die erste Anfrage mit rückwärts laufendem Zeitfenster wurde dagegen richtig abgelehnt, sonst wäre der Test schon dort gescheitert (1 Punkt).

  3. Zum Beispiel (je Zeile 1 Punkt, Entscheidung nur mit Begründung):

Einträge im Abnahmeprotokoll
Abnahmeprotokoll US-09
Ergebnis 3 von 4 Abnahmekriterien erfüllt; AK-09.2 teilweise nicht erfüllt
Mängel Zeitfenster über 90 Minuten werden angenommen (Beispiel 06:00 bis 07:45 Uhr ergibt 201)
Entscheidung nicht abgenommen; Nachbesserung der Zeitfensterregel und erneuter Abnahmetest. Begründung: Ein vereinbartes Kriterium ist verletzt, und zu lange Gesuche erscheinen sonst als Daten im System.

Auch „abgenommen mit Auflage“ ist vertretbar, wenn die Auftraggeber*innen den Mangel als gering einstufen und eine Frist für die Nachbesserung festgehalten wird. Ausgelöst wurde der Fehler im Projekt mit einer Grenze von 180 statt 90 Minuten; der Komponententest der Zeitfensterregel hätte ihn mit dem Grenzwert 91 Minuten schon früher gefunden.

Aufgabe 2.1: Einen Abnahmetest für AK-09.1 schreiben (Kernkompetenz)

Schreibe in der Klasse GesuchAbnahmeTest extends SystemtestBasis die Testmethode für AK-09.1 mit passendem @DisplayName. Oksana stellt am Dienstag, dem 20.10.2026, ein Gesuch für Mittwoch, den 21.10.2026, ein. Der Test soll prüfen, dass die Antwort 201 lautet, und das Gesuch danach über den Location-Header abrufen und prüfen, dass Mitfahrerin, Datum, Beginn des Zeitfensters und Status stimmen. Importe musst Du nicht notieren. (7 Punkte)

    @Test
    @DisplayName("AK-09.1 Oksana stellt für morgen ein Gesuch ein: gespeichert, abrufbar, offen")
    void ak0901() {
        String location = client.post().uri("/v1/gesuche").contentType(MediaType.APPLICATION_JSON)
                .body("""
                        {"mitfahrerId": 3, "haltepunktId": 1, "richtung": "zur-schule", "datum": "2026-10-21",
                         "zeitVon": "06:45", "zeitBis": "07:15"}
                        """)
                .exchange()
                .expectStatus().isCreated()
                .returnResult().getResponseHeaders().getLocation().toString();

        client.get().uri(location).exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.mitfahrerId").isEqualTo(3)
                .jsonPath("$.datum").isEqualTo("2026-10-21")
                .jsonPath("$.zeitVon").isEqualTo("06:45")
                .jsonPath("$.status").isEqualTo("offen");
    }

Punkte: @Test und @DisplayName mit der Nummer des Kriteriums (1), POST mit Content-Type und JSON-Body nach dem Vertrag (2), expectStatus().isCreated() (1), Location aus der Antwort lesen (1), GET auf diese Adresse (1), Prüfungen von Mitfahrerin, Datum, Zeit und Status (1). Statt jsonPath ist auch returnResult(Gesuch.class) mit dem erzeugten Modell und AssertJ richtig. Im Projekt ausgeführt ist der Test grün.

Wichtig ist das Datum: „morgen“ heißt hier 21.10.2026, weil die Uhr in SystemtestBasis feststeht. Der Abruf über Location prüft zugleich, dass das Gesuch wirklich gespeichert ist und nicht nur in der Antwort steht.

Aufgabe 2.2: Ablehnungen und Zurückziehen abnehmen (Kernkompetenz)

Ergänze GesuchAbnahmeTest um zwei Testmethoden. Nutze dafür die Hilfsmethoden

    private RestTestClient.ResponseSpec stelleEin(String datum, String von, String bis) { /* POST /v1/gesuche für Oksana, HP 1 */ }

    private RestTestClient.ResponseSpec zieheZurueck(String location) { /* PATCH mit {"status": "zurueckgezogen"} */ }
  1. AK-09.2: Prüfe beide Fälle des Kriteriums mit je einer Anfrage für den 21.10.2026 auf Status und type der Antwort. Die Anwendung meldet beide Fälle als https://mitfahrlabor.example/probleme/zeitfenster-ungueltig. (3 Punkte)

  2. AK-09.4: Lege ein eigenes Gesuch für den 22.10.2026 an, ziehe es zurück und prüfe den Status; ziehe es danach ein zweites Mal zurück und prüfe Status und type (gesuch-nicht-offen). Begründe, warum der Test nicht Gesuch 1 aus data.sql verwendet. (4 Punkte)

  1. (3 Punkte: zwei passende Zeitfenster 1, Status 1, type 1)
    @Test
    @DisplayName("AK-09.2 Rückwärts laufendes oder zu langes Zeitfenster wird abgelehnt")
    void ak0902() {
        stelleEin("2026-10-21", "07:30", "07:15").expectStatus().isEqualTo(422)
                .expectBody().jsonPath("$.type")
                .isEqualTo("https://mitfahrlabor.example/probleme/zeitfenster-ungueltig");
        stelleEin("2026-10-21", "06:00", "07:45").expectStatus().isEqualTo(422)
                .expectBody().jsonPath("$.type")
                .isEqualTo("https://mitfahrlabor.example/probleme/zeitfenster-ungueltig");
    }
  1. (4 Punkte: eigenes Gesuch anlegen und Location merken 1, erstes Zurückziehen mit Status 1, zweites mit 409 und type 1, Begründung 1)
    @Test
    @DisplayName("AK-09.4 Zurückziehen: Status „zurückgezogen“, zweites Zurückziehen abgelehnt")
    void ak0904() {
        String location = stelleEin("2026-10-22", "06:45", "07:15").expectStatus().isCreated()
                .returnResult().getResponseHeaders().getLocation().toString();

        zieheZurueck(location).expectStatus().isOk()
                .expectBody().jsonPath("$.status").isEqualTo("zurueckgezogen");
        zieheZurueck(location).expectStatus().isEqualTo(409)
                .expectBody().jsonPath("$.type")
                .isEqualTo("https://mitfahrlabor.example/probleme/gesuch-nicht-offen");
    }

Begründung: Alle System- und Abnahmetests teilen sich einen Spring-Kontext und damit eine Datenbank, und die Änderungen im Server werden nicht zurückgerollt. Würde der Test Gesuch 1 zurückziehen, wäre es für alle späteren Tests nicht mehr offen; ein anderer Test, der Gesuch 1 erwartet, würde je nach Reihenfolge rot. Beide Tests sind im Projekt grün.

Aufgabe 3.1: Grün am 10.11., rot am 24.11. (Profilkompetenz)

Ein Teammitglied hat am 10.11.2026 einen Abnahmetest ohne SystemtestBasis geschrieben, um die Startdaten aus data.sql zu nutzen:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Tag("system")
class GesuchOhneFesteUhrTest {

    @Autowired
    private RestTestClient client;

    @Test
    void gesuchWirdEingestellt() {
        client.post().uri("/v1/gesuche").contentType(MediaType.APPLICATION_JSON)
                .body("""
                        {"mitfahrerId": 3, "haltepunktId": 1, "richtung": "zur-schule", "datum": "2026-11-23",
                         "zeitVon": "06:45", "zeitBis": "07:15"}
                        """)
                .exchange()
                .expectStatus().isCreated();
    }
}

Der Test ist zwei Wochen lang grün. Am Dienstag, dem 24.11.2026, ist die Pipeline rot, obwohl niemand etwas geändert hat:

java.lang.AssertionError: Status expected:<201 CREATED> but was:<422 UNPROCESSABLE_CONTENT>
...
< 422 UNPROCESSABLE_CONTENT Unprocessable Content
< content-type: [application/problem+json]

{..."status":422,"title":"Fachregel verletzt","type":"https://mitfahrlabor.example/probleme/gesuch-unzulaessig"}
  1. Erkläre die Ursache. (2 Punkte)

  2. Ändere den Test so, dass er unabhängig vom Tag des Testlaufs dasselbe Ergebnis liefert. Gib nur die geänderten oder neuen Zeilen an. (3 Punkte)

  3. Ein anderes Teammitglied schlägt vor, einfach das Datum 2030-01-01 zu verwenden. Beurteile den Vorschlag. (1 Punkt)

  1. Der Test nutzt die echte Uhr der Anwendung, weil er die Clock-Bean nicht ersetzt (1 Punkt). Bis zum 23.11. liegt das Datum des Gesuchs heute oder in der Zukunft; ab dem 24.11. liegt es in der Vergangenheit, und der GesuchService lehnt das Gesuch nach AK-09.3 mit 422 ab. Der Test hängt also vom Kalender ab, nicht vom Code (1 Punkt).

  2. Die Klasse von SystemtestBasis erben lassen (class GesuchOhneFesteUhrTest extends SystemtestBasis, dann entfallen die Annotationen und das Feld client), oder die Uhr in der Klasse selbst festsetzen (3 Punkte: @TestBean-Feld 1, statische Fabrikmethode mit gleichem Namen 1, feste Uhr vor dem 23.11.2026 in der Zeitzone der Anwendung 1):

    @TestBean
    private Clock uhr;

    static Clock uhr() {
        return Clock.fixed(Instant.parse("2026-11-20T06:00:00Z"), UhrKonfiguration.ZEITZONE);
    }

Nachgestellt wurde der Fehler im Projekt mit derselben Konstruktion und einer Uhr auf dem 24.11.2026; die Ausgabe oben stammt aus diesem Lauf.

  1. Der Test wäre dann erst ab 2030 rot, das Problem ist nur verschoben (1 Punkt). Außerdem prüft er so nicht mehr den typischen Fall „morgen“, und Kriterien, die vom heutigen Tag abhängen (AK-09.3, Flurmonitor ab heute), lassen sich ohne feste Uhr gar nicht verlässlich testen.

Aufgabe 3.2: Ein zweites Frontend freigeben (Profilkompetenz)

Das Vue-Frontend läuft in der Entwicklung unter http://localhost:5173. Für einen letzten Test vor dem Ausliefern startet das Team zusätzlich die Vorschau des fertigen Builds unter http://localhost:4173. Dort meldet der Browser beim Einstellen eines Gesuchs einen CORS-Fehler. Mit der .http-Datei funktioniert dieselbe Anfrage.

  1. Erkläre, warum die Anfrage aus der .http-Datei funktioniert, aus dem Browser aber nicht. (2 Punkte)

  2. Gib die Änderung an, mit der beide Frontends die API nutzen dürfen, ohne Java-Code zu ändern. (1 Punkt)

  3. Schreibe für CorsSystemTest einen Test, der die Vorabanfrage der Vorschau für POST /v1/gesuche prüft. (3 Punkte)

  1. Die Same-Origin-Policy setzen nur Browser durch: Sie schicken vor dem POST mit JSON eine Vorabanfrage (OPTIONS) mit Origin: http://localhost:4173 und geben die Antwort nur frei, wenn der Server diese Herkunft in Access-Control-Allow-Origin nennt (1 Punkt). Die .http-Datei (oder curl) fragt nicht vorab und wertet keine CORS-Header aus. Die Herkunft http://localhost:4173 unterscheidet sich im Port von der erlaubten Herkunft, deshalb lehnt die Anwendung sie mit 403 ab (1 Punkt).

  2. In application.properties beide Herkünfte mit Komma eintragen (1 Punkt):

mitfahrlabor.cors.erlaubte-urspruenge=http://localhost:5173,http://localhost:4173

Die CorsKonfiguration liest die Eigenschaft als String[]; Spring trennt die Werte am Komma. Ein * wäre bequemer, gibt die API aber für jede Webseite frei und ist nicht mehr möglich, sobald Anmeldedaten mitgeschickt werden.

  1. (3 Punkte: OPTIONS mit Origin und Access-Control-Request-Method 1, Status 1, Access-Control-Allow-Origin mit der Vorschau-Adresse 1)
    @Test
    void vorschauDesFrontendsDarfGesucheEinstellen() {
        client.options().uri("/v1/gesuche")
                .header(HttpHeaders.ORIGIN, "http://localhost:4173")
                .header(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST")
                .exchange()
                .expectStatus().isOk()
                .expectHeader().valueEquals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://localhost:4173");
    }

Mit der Änderung aus b) ist der Test im Projekt grün, ebenso die drei bisherigen Tests in CorsSystemTest. Weil das Mapping /v1/** lautet, gilt die Konfiguration ohne weitere Änderung auch für die Gesuch-Endpunkte.


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 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu System- und Abnahmetest: Die Mitfahrgesuche abnehmen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/spring-boot-systemtest-abnahmetest-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: