Fachlogik in reinem Java: Komponententests für Kostenbeitrag, Einwilligung und Buchungsstatus

https://bildung.social/@oerinformatik/

https://oer-informatik.de/spring-boot-fachlogik-komponententest

Komponententest, JUnit, Fachlogik, Äquivalenzklassen, Grenzwertanalyse, BigDecimal, LocalDate, Enum, Exception, Spring Boot

tl/dr; (ca. 19 min Lesezeit): Die Regeln, die der API-Vertrag nicht ausdrücken kann, kommen ins Paket domain: der Kostenbeitrag je Kilometer, die Einwilligung bei Minderjährigen und die erlaubten Wechsel des Buchungsstatus. Sie entstehen in reinem Java ohne Spring, mit BigDecimal, LocalDate, einem switch-Ausdruck und eigenen Exceptions. Mit Äquivalenzklassen und Grenzwerten findest Du die Testfälle und prüfst sie mit parametrisierten JUnit-Tests: die erste Testebene des V-Modells, der Komponententest [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 / Übungsaufgaben zu 04

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste der Artikel Vom Auftrag zur API, Unit-Tests und Blackbox-Testfälle auf.

Als Komponententest-Grundlagen kann ich nach Durchlesen des Artikels…

  • … begründen, warum Fachregeln in einem eigenen Paket ohne Abhängigkeit zu Spring liegen.

  • … erklären, warum Geldbeträge mit BigDecimal statt mit double berechnet werden.

  • … für eine Fachregel Äquivalenzklassen bilden und die Grenzwerte als Testfälle bestimmen.

  • … einen JUnit-Test mit @Test, assertEquals, assertThrows und assertDoesNotThrow notieren.

  • … einen parametrisierten Test mit @ParameterizedTest und @CsvSource notieren.

  • … das Alter an einem Stichtag mit LocalDate und Period berechnen.

  • … erlaubte Zustandswechsel mit einem switch-Ausdruck über eine Aufzählung umsetzen und vollständig testen.

Darüber hinaus kann ich als Komponententest-Expertenwissen…

  • … eine Hierarchie eigener, ungeprüfter Exceptions entwerfen und begründen.

  • … die Meldung eines fehlgeschlagenen Tests deuten und die Ursache im Code finden.

In Artikel 03 hat das Backend ein Angebot mit 5,00 Euro Kostenbeitrag ab Gütersloh angenommen, obwohl dort höchstens 1,86 Euro erlaubt sind. Der API-Vertrag kann diese Regel nicht prüfen, denn die Grenze hängt von der Entfernung des Haltepunkts ab. Solche Fachregeln hat der Modulentwurf in Artikel 01 festgehalten. Jetzt werden sie zu Java-Code, und zwar so, dass sie sich einzeln und schnell testen lassen. Damit beginnt der rechte Ast des V-Modells: Der Komponententest prüft die kleinsten Bausteine gegen den Modulentwurf.

Fahrplan der Artikelreihe im V-Modell, hervorgehoben ist der Komponententest, zu dem die Artikel 04 und 08 gehören (mit KI erstellt)

Ausgangspunkt ist das Projekt aus Artikel 03. Das Ergebnis findest Du im Ordner src/04-fachlogik-komponententest des Repositorys. Was einen guten Unit-Test ausmacht und wie JUnit grundsätzlich funktioniert, steht im Artikel Unit-Tests; hier wenden wir es an.

Warum die Fachlogik ohne Spring auskommt

Laut Architekturentwurf liegt die Fachlogik im Paket domain, und dieses Paket hängt von keiner anderen Schicht ab: kein Spring, keine Datenbank, kein HTTP. Das hat zwei Vorteile. Erstens ändern sich Fachregeln aus anderen Gründen als Technik; wer den Webserver austauscht, soll die Kostenregel nicht anfassen müssen. Zweitens lässt sich reiner Java-Code ohne Aufwand testen: Ein Test ruft eine Methode auf und prüft das Ergebnis. Es muss keine Anwendung starten, und alle Tests dieses Artikels laufen zusammen in deutlich unter einer Sekunde.

Für die ersten drei Regeln entstehen drei Bausteine und eine Familie von Exceptions:

UML-Klassendiagramm des Pakets domain: Kostenregel mit hoechstbetrag() und pruefe(), Einwilligungsregel mit istMinderjaehrig() und pruefe(), Aufzählung Buchungsstatus mit erlaubtWechselZu() und wechsleZu(); jede erzeugt eine eigene Exception; KostenbeitragZuHochException und EinwilligungFehltException erben von FachregelVerletztException, StatuswechselUnzulaessigException von ZustandskonfliktException, beide von MitfahrlaborException, diese von RuntimeException (mit KI erstellt)

Kostenregel und Einwilligungsregel haben nur statische Methoden und einen privaten Konstruktor, damit niemand unnötig Objekte davon erzeugt. Verletzt eine Eingabe eine Regel, wirft die Methode eine Exception. Die Exceptions sind in zwei Zweigen geordnet, die sich später auf die Statuscodes aus Artikel 01 abbilden lassen: FachregelVerletztException wird zu 422, ZustandskonfliktException zu 409. Alle erben von RuntimeException und sind damit ungeprüft (unchecked): Wer eine solche Methode aufruft, muss sie nicht mit try/catch umschließen. Das passt, weil der Aufrufer, später der Service, den Fehler meist nicht beheben kann, sondern ihn bis zur Web-Schicht durchreicht. So sieht eine der Exceptions aus:

public abstract class MitfahrlaborException extends RuntimeException {

    protected MitfahrlaborException(String meldung) {
        super(meldung);
    }
}

public abstract class FachregelVerletztException extends MitfahrlaborException {

    protected FachregelVerletztException(String meldung) {
        super(meldung);
    }
}

public class KostenbeitragZuHochException extends FachregelVerletztException {

    public KostenbeitragZuHochException(BigDecimal beitrag, BigDecimal hoechstbetrag) {
        super("Kostenbeitrag " + beitrag + " Euro ist zu hoch, erlaubt sind höchstens "
                + hoechstbetrag + " Euro je Person.");
    }
}

Jede Klasse steht in einer eigenen Datei; hier sind sie nur zum Lesen zusammengefasst.

Regel 1: Kostenbeitrag mit BigDecimal

Die Regel lautet: höchstens 0,10 Euro je Kilometer und Person. Mit double zu rechnen liegt nahe, ist bei Geld aber gefährlich. Ein double speichert Zahlen im Zweiersystem, und viele Dezimalbrüche wie 0,1 lassen sich dort nicht exakt darstellen. In Java ergibt 0.1 + 0.2 deshalb 0.30000000000000004. Bei Vergleichen an einer Grenze kann so ein winziger Rest entscheiden, ob ein Beitrag erlaubt ist.

Die Klasse BigDecimal rechnet dagegen exakt im Zehnersystem. Zwei Regeln sind dabei wichtig: Werte entstehen aus Texten (new BigDecimal("0.10")), nicht aus double-Literalen, und verglichen wird mit compareTo(), nicht mit equals(). Denn equals() berücksichtigt auch die Zahl der Nachkommastellen, sodass 1.8 und 1.80 als verschieden gelten.

package de.dbh.mitfahrlabor.domain;

import java.math.BigDecimal;
import java.math.RoundingMode;

public final class Kostenregel {

    public static final BigDecimal EURO_JE_KM = new BigDecimal("0.10");

    private Kostenregel() {
        // nur statische Methoden
    }

    /** Höchster erlaubter Beitrag, auf Cent abgerundet, damit die Grenze nie überschritten wird. */
    public static BigDecimal hoechstbetrag(BigDecimal entfernungKm) {
        return entfernungKm.multiply(EURO_JE_KM).setScale(2, RoundingMode.DOWN);
    }

    public static void pruefe(BigDecimal beitrag, BigDecimal entfernungKm) {
        if (beitrag.signum() < 0) {
            throw new IllegalArgumentException("Der Kostenbeitrag darf nicht negativ sein.");
        }
        BigDecimal hoechstbetrag = hoechstbetrag(entfernungKm);
        if (beitrag.compareTo(hoechstbetrag) > 0) {
            throw new KostenbeitragZuHochException(beitrag, hoechstbetrag);
        }
    }
}

setScale(2, RoundingMode.DOWN) bringt den Höchstbetrag auf zwei Nachkommastellen und schneidet dabei ab. Bei 18,65 km ergeben sich rechnerisch 1,865 Euro; kaufmännisch gerundet wären das 1,87 Euro und damit mehr als 0,10 Euro je Kilometer. Abgerundet sind es 1,86 Euro, und die Regel ist sicher eingehalten. Ein negativer Beitrag ist kein Verstoß gegen die Fachregel, sondern ein unsinniger Wert; er löst die allgemeine IllegalArgumentException aus.

Testfälle finden: Äquivalenzklassen und Grenzwerte

Welche Werte soll der Test prüfen? Alle möglichen Beträge auszuprobieren ist unmöglich. Die Methoden aus dem Artikel Blackbox-Testfälle helfen: Werte, die das Programm gleich behandeln sollte, bilden eine Äquivalenzklasse; aus jeder Klasse genügt ein Vertreter. Fehler stecken aber besonders oft an den Rändern der Klassen, zum Beispiel in einem > statt >=. Deshalb kommen die Grenzwerte direkt an und neben jeder Grenze hinzu.

Äquivalenzklassen und Grenzwerte: oben der Kostenbeitrag ab Gütersloh mit den Klassen „ungültig: negativ“ (Vertreter -0,50, Grenzwert -0,01), „gültig: 0,00 bis 1,86“ (Grenzwerte 0,00 und 1,86, Vertreter 1,80) und „ungültig: zu hoch“ (Grenzwert 1,87, Vertreter 5,00); unten das Fahrtdatum bei Aylin Kaya ohne Einwilligung mit den Klassen minderjährig (Vertreter 19.11.2026, Grenzwert 01.06.2027) und volljährig (Grenzwerte 02.06.2027 und 03.06.2027) (mit KI erstellt)

Für den Haltepunkt Gütersloh mit 18,6 km ergeben sich diese Testfälle:

Äquivalenzklassen und Grenzwerte für den Kostenbeitrag ab Gütersloh
Äquivalenzklasse Bedingung Vertreter Grenzwerte erwartetes Ergebnis
ungültig: negativ Beitrag < 0,00 -0,50 -0,01 IllegalArgumentException
gültig 0,00 ≤ Beitrag ≤ 1,86 1,80 0,00 und 1,86 keine Exception
ungültig: zu hoch Beitrag > 1,86 5,00 1,87 KostenbeitragZuHochException

Dazu kommen Testfälle für hoechstbetrag() selbst: mehrere Haltepunkte aus dem Szenario, die Rundung bei 18,65 km und der Rand 0 km.

Der erste Komponententest mit JUnit

Die Testklasse liegt im gleichen Paket wie die getestete Klasse, aber unter src/test/java. Sie braucht kein Spring, keine Annotation an der Klasse und keinen Start der Anwendung:

package de.dbh.mitfahrlabor.domain;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.math.BigDecimal;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class KostenregelTest {

    private static final BigDecimal GUETERSLOH_KM = new BigDecimal("18.6");

    @ParameterizedTest(name = "{0} km -> höchstens {1} Euro")
    @CsvSource({
            "18.6,  1.86",
            "13.1,  1.31",
            "6.2,   0.62",
            "18.65, 1.86",
            "0.0,   0.00"
    })
    @DisplayName("Höchstbetrag: 0,10 Euro je km, auf Cent abgerundet")
    void hoechstbetrag(BigDecimal entfernungKm, BigDecimal erwartet) {
        // when
        BigDecimal hoechstbetrag = Kostenregel.hoechstbetrag(entfernungKm);
        // then
        assertEquals(erwartet, hoechstbetrag);
    }

    @ParameterizedTest(name = "{0} Euro ist erlaubt")
    @CsvSource({ "0.00", "1.80", "1.86" })
    @DisplayName("gültige Beiträge bis einschließlich zur Grenze")
    void gueltigerBeitrag(BigDecimal beitrag) {
        assertDoesNotThrow(() -> Kostenregel.pruefe(beitrag, GUETERSLOH_KM));
    }

    @ParameterizedTest(name = "{0} Euro ist zu hoch")
    @CsvSource({ "1.87", "5.00" })
    @DisplayName("Beitrag über der Grenze wird abgelehnt")
    void zuHoherBeitrag(BigDecimal beitrag) {
        assertThrows(KostenbeitragZuHochException.class,
                () -> Kostenregel.pruefe(beitrag, GUETERSLOH_KM));
    }

    @ParameterizedTest(name = "{0} Euro ist negativ")
    @CsvSource({ "-0.01", "-0.50" })
    @DisplayName("negativer Beitrag ist kein gültiger Wert")
    void negativerBeitrag(BigDecimal beitrag) {
        assertThrows(IllegalArgumentException.class,
                () -> Kostenregel.pruefe(beitrag, GUETERSLOH_KM));
    }

    @Test
    @DisplayName("Fehlermeldung nennt Beitrag und Höchstbetrag")
    void meldung() {
        KostenbeitragZuHochException fehler = assertThrows(KostenbeitragZuHochException.class,
                () -> Kostenregel.pruefe(new BigDecimal("1.90"), GUETERSLOH_KM));
        assertEquals("Kostenbeitrag 1.90 Euro ist zu hoch, erlaubt sind höchstens 1.86 Euro je Person.",
                fehler.getMessage());
    }
}

Die Bausteine im Einzelnen:

  • @Test kennzeichnet eine Testmethode mit festen Werten. @ParameterizedTest führt dieselbe Methode mehrfach aus, einmal je Zeile der Datenquelle.

  • @CsvSource liefert die Daten als kommagetrennte Zeilen. JUnit wandelt die Texte selbst in die Parametertypen um, hier in BigDecimal. So steht jede Zeile der Testfalltabelle als eine Zeile im Test.

  • @DisplayName und das Attribut name geben den Tests lesbare Namen, die im Testbericht erscheinen, etwa "18.65" km -> höchstens "1.86" Euro.

  • assertEquals(erwartet, tatsaechlich) vergleicht zwei Werte. Weil hoechstbetrag() immer genau zwei Nachkommastellen liefert, funktioniert hier auch der Vergleich zweier BigDecimal mit equals().

  • assertThrows(Typ.class, () -> …) erwartet, dass der übergebene Lambda-Ausdruck eine Exception dieses Typs wirft, und gibt sie zurück, sodass Du auch ihre Meldung prüfen kannst. assertDoesNotThrow(…) prüft das Gegenteil.

Wie wichtig die Grenzwerte sind, zeigt ein Experiment: Ändere RoundingMode.DOWN in RoundingMode.HALF_UP und starte die Tests. Genau ein Test schlägt fehl:

KostenregelTest > Höchstbetrag: 0,10 Euro je km, auf Cent abgerundet > "18.65" km -> höchstens "1.86" Euro FAILED
    org.opentest4j.AssertionFailedError: expected: <1.86> but was: <1.87>

Die Meldung nennt den Testfall, den erwarteten und den tatsächlichen Wert. Ohne den Testfall mit 18,65 km wäre der Fehler unbemerkt geblieben, denn alle anderen Entfernungen haben nur eine Nachkommastelle.

Regel 3: Einwilligung mit LocalDate und Period

Wer am Tag der Fahrt noch nicht 18 ist, braucht die Einwilligung der Erziehungsberechtigten. Entscheidend ist das Alter am Fahrtdatum, nicht heute. Das Alter in ganzen Jahren liefert die Klasse Period:

package de.dbh.mitfahrlabor.domain;

import java.time.LocalDate;
import java.time.Period;

public final class Einwilligungsregel {

    public static final int VOLLJAEHRIG_AB_JAHREN = 18;

    private Einwilligungsregel() {
        // nur statische Methoden
    }

    public static boolean istMinderjaehrig(LocalDate geburtsdatum, LocalDate stichtag) {
        return Period.between(geburtsdatum, stichtag).getYears() < VOLLJAEHRIG_AB_JAHREN;
    }

    public static void pruefe(LocalDate geburtsdatum, LocalDate einwilligungEltern, LocalDate fahrtdatum) {
        if (istMinderjaehrig(geburtsdatum, fahrtdatum) && einwilligungEltern == null) {
            throw new EinwilligungFehltException(fahrtdatum);
        }
    }
}

Liegt keine Einwilligung vor, wird null übergeben; im Fachmodell hat das Attribut die Multiplizität [0..1]. Würde man die Jahre selbst aus der Differenz der Jahreszahlen berechnen, wäre eine Person, die im Dezember Geburtstag hat, schon im Januar ein Jahr älter. Period.between() berücksichtigt Monat und Tag. Die Grenze liegt beim 18. Geburtstag:

class EinwilligungsregelTest {

    // Aylin Kaya aus dem Szenario: geboren am 02.06.2009, wird am 02.06.2027 volljährig
    private static final LocalDate AYLIN_GEBURTSTAG = LocalDate.of(2009, 6, 2);

    @ParameterizedTest(name = "Fahrt am {0}: minderjährig = {1}")
    @CsvSource({
            "2026-11-19, true",
            "2027-06-01, true",
            "2027-06-02, false",
            "2027-06-03, false"
    })
    @DisplayName("Grenze ist der 18. Geburtstag")
    void minderjaehrigBisZumAchtzehntenGeburtstag(LocalDate fahrtdatum, boolean erwartet) {
        assertEquals(erwartet, Einwilligungsregel.istMinderjaehrig(AYLIN_GEBURTSTAG, fahrtdatum));
    }

    @ParameterizedTest(name = "geboren 29.02.2008, Stichtag {0}: minderjährig = {1}")
    @CsvSource({
            "2026-02-28, true",
            "2026-03-01, false"
    })
    @DisplayName("Sonderfall: Geburtstag am 29. Februar")
    void schaltjahrGeburtstag(LocalDate stichtag, boolean erwartet) {
        assertEquals(erwartet, Einwilligungsregel.istMinderjaehrig(LocalDate.of(2008, 2, 29), stichtag));
    }

    @Test
    @DisplayName("minderjährig ohne Einwilligung: abgelehnt")
    void minderjaehrigOhneEinwilligung() {
        assertThrows(EinwilligungFehltException.class, () -> Einwilligungsregel.pruefe(
                AYLIN_GEBURTSTAG, null, LocalDate.of(2026, 11, 19)));
    }

    // … dazu „minderjährig mit Einwilligung“ und „volljährig ohne Einwilligung“
}

Daten im Format 2027-06-02 wandelt JUnit selbst in LocalDate um. Der Sonderfall 29. Februar zeigt, warum man fertige Datumsklassen nutzt: Wer an einem Schalttag geboren ist, wird in Jahren ohne 29. Februar erst am 1. März 18. Period.between() rechnet das richtig. Mit einwilligungEltern ergeben sich insgesamt vier Kombinationen aus „minderjährig ja/nein“ und „Einwilligung ja/nein“; nur eine davon ist verboten. Im Projekt prüfen drei Testmethoden die verbotene Kombination sowie „minderjährig mit Einwilligung“ und „volljährig ohne Einwilligung“.

Regel 6: Statuswechsel mit switch-Ausdruck

Das Zustandsdiagramm der Buchung aus Artikel 01 legt fest, welche Wechsel erlaubt sind:

UML-Zustandsdiagramm einer Buchung: vom Start nach angefragt; von angefragt mit bestaetigen() und Wächter freiePlaetze > 0 nach bestätigt, mit ablehnen() nach abgelehnt, mit stornieren() nach storniert; von bestätigt mit stornieren() nach storniert; abgelehnt und storniert enden (mit KI erstellt)

Im Code wird es zu einer Aufzählung mit einer Methode. Wächter und Effekte (freie Plätze) gehören nicht dazu, sie prüft später die Service-Schicht:

package de.dbh.mitfahrlabor.domain;

public enum Buchungsstatus {
    ANGEFRAGT, BESTAETIGT, ABGELEHNT, STORNIERT;

    public boolean erlaubtWechselZu(Buchungsstatus ziel) {
        return switch (this) {
            case ANGEFRAGT -> ziel == BESTAETIGT || ziel == ABGELEHNT || ziel == STORNIERT;
            case BESTAETIGT -> ziel == STORNIERT;
            case ABGELEHNT, STORNIERT -> false;
        };
    }

    public Buchungsstatus wechsleZu(Buchungsstatus ziel) {
        if (!erlaubtWechselZu(ziel)) {
            throw new StatuswechselUnzulaessigException(this, ziel);
        }
        return ziel;
    }
}

Der switch-Ausdruck (mit -> statt case …: und break) liefert einen Wert. Weil er über eine Aufzählung läuft, prüft der Compiler, ob alle Konstanten behandelt sind: Kommt später ein fünfter Status hinzu, lässt sich der Code erst wieder übersetzen, wenn der neue Fall eine Zeile bekommt. Diese Aufzählung ist Teil der Domäne und unabhängig von der erzeugten Aufzählung BuchungStatus im Paket api.model; die Übersetzung zwischen beiden folgt in Artikel 09.

Für Zustandsautomaten gibt es ein eigenes Testverfahren, den Zustandsübergangstest. Er prüft nicht nur die erlaubten Übergänge aus dem Diagramm, sondern jede Kombination aus Ausgangs- und Zielzustand. Bei vier Zuständen sind das 16 Fälle, von denen vier erlaubt sind:

Erlaubte Wechsel des Buchungsstatus (Zustandsübergangstabelle)
von \ nach ANGEFRAGT BESTAETIGT ABGELEHNT STORNIERT
ANGEFRAGT nein ja ja ja
BESTAETIGT nein nein nein ja
ABGELEHNT nein nein nein nein
STORNIERT nein nein nein nein

Die Tabelle wird fast wörtlich zur @CsvSource; JUnit wandelt die Namen der Konstanten selbst in Werte der Aufzählung um:

@ParameterizedTest(name = "{0} -> {1}: erlaubt = {2}")
@CsvSource({
        "ANGEFRAGT,  ANGEFRAGT,  false",
        "ANGEFRAGT,  BESTAETIGT, true",
        "ANGEFRAGT,  ABGELEHNT,  true",
        "ANGEFRAGT,  STORNIERT,  true",
        "BESTAETIGT, ANGEFRAGT,  false",
        // … alle 16 Kombinationen
        "STORNIERT,  STORNIERT,  false"
})
@DisplayName("alle 16 Kombinationen aus Ausgangs- und Zielzustand")
void erlaubteWechsel(Buchungsstatus von, Buchungsstatus nach, boolean erwartet) {
    assertEquals(erwartet, von.erlaubtWechselZu(nach));
}

Zwei weitere Tests prüfen, dass wechsleZu() bei einem erlaubten Wechsel den neuen Zustand liefert und bei einem verbotenen die StatuswechselUnzulaessigException mit passender Meldung wirft.

Die Tests ausführen

Die Tests laufen in der Entwicklungsumgebung über das Symbol neben Klasse oder Methode oder im Build:

.\gradlew.bat test

Maven listet die Ergebnisse je Testklasse auf:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in de.dbh.mitfahrlabor.MitfahrlaborApplicationTests
Tests run: 13, Failures: 0, Errors: 0, Skipped: 0 -- in de.dbh.mitfahrlabor.domain.KostenregelTest
Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in de.dbh.mitfahrlabor.domain.EinwilligungsregelTest
Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 -- in de.dbh.mitfahrlabor.domain.BuchungsstatusTest
Tests run: 41, Failures: 0, Errors: 0, Skipped: 0

Die 40 Komponententests brauchen zusammen weniger als eine halbe Sekunde, der Smoke-Test allein mehrere Sekunden, weil er die ganze Anwendung startet. Gradle gibt bei Erfolg nur BUILD SUCCESSFUL aus; den ausführlichen Bericht mit den lesbaren Testnamen öffnest Du unter build/reports/tests/test/index.html.

Was die Komponententests nicht prüfen

Die Tests zeigen, dass jede Regel für sich stimmt. Sie zeigen nicht, dass die Regeln auch angewendet werden: Noch ruft kein Controller Kostenregel.pruefe() auf, und das Angebot mit 5,00 Euro würde weiterhin angenommen. Das Zusammenspiel von Regeln, gespeicherten Daten und Statuswechseln, etwa „bestätigen senkt die freien Plätze, bei 0 ist das Angebot voll“, gehört in die Service-Schicht und wird in Artikel 08 mit einem zweiten Komponententest geprüft, dann mit Test-Doubles für die Datenbank.

Nächster Artikel

Jetzt gibt es Tests, aber sie laufen nur, wenn jemand daran denkt. Im nächsten Artikel ergänzt Du den Build um Prüfungen der Codequalität, einen Linter, einheitliche Formatierung und einen Bericht über die Testabdeckung, und richtest eine CI-Pipeline in GitLab ein, die alles bei jedem Push ausführt.

Leitfragen

  1. Warum liegen die Fachregeln im Paket domain ohne Abhängigkeit zu Spring?

Fachregeln ändern sich aus anderen Gründen als die Technik und sollen davon unabhängig bleiben. Außerdem lässt sich reiner Java-Code ohne Start der Anwendung in Millisekunden testen. siehe Abschnitt „Warum die Fachlogik ohne Spring auskommt“

  1. Warum rechnet die Kostenregel mit BigDecimal und wie werden zwei Beträge verglichen?

double kann viele Dezimalbrüche nicht exakt darstellen, sodass an Grenzen kleine Rundungsreste entscheiden. BigDecimal rechnet exakt im Zehnersystem; Werte entstehen aus Texten, verglichen wird mit compareTo(), weil equals() auch die Zahl der Nachkommastellen vergleicht. siehe Abschnitt „Regel 1: Kostenbeitrag mit BigDecimal“

  1. Welche Testfälle ergeben Äquivalenzklassen und Grenzwerte für den Kostenbeitrag ab Gütersloh?

Drei Klassen (negativ, 0,00 bis 1,86, über 1,86) mit je einem Vertreter, etwa -0,50, 1,80 und 5,00, dazu die Grenzwerte -0,01, 0,00, 1,86 und 1,87. siehe Abschnitt „Testfälle finden: Äquivalenzklassen und Grenzwerte“

  1. Wozu dienen @ParameterizedTest und @CsvSource?

@ParameterizedTest führt eine Testmethode mehrfach mit verschiedenen Daten aus; @CsvSource liefert die Daten als kommagetrennte Zeilen, die JUnit in die Parametertypen umwandelt. So wird jede Zeile einer Testfalltabelle zu einem Testlauf. siehe Abschnitt „Der erste Komponententest mit JUnit“

  1. Wie prüft ein Test, dass eine bestimmte Exception geworfen wird?

Mit assertThrows(Typ.class, () -> aufruf()). Der Test schlägt fehl, wenn keine oder eine andere Exception geworfen wird; zurückgegeben wird die Exception, deren Meldung man anschließend prüfen kann. siehe Abschnitt „Der erste Komponententest mit JUnit“

  1. Warum berechnet die Einwilligungsregel das Alter mit Period.between() und nicht über die Differenz der Jahreszahlen?

Die Differenz der Jahreszahlen ignoriert Monat und Tag; eine Person wäre dann vor ihrem Geburtstag schon ein Jahr älter. Period.between() liefert die vollen Jahre bis zum Stichtag, auch bei Geburtstagen am 29. Februar. siehe Abschnitt „Regel 3: Einwilligung mit LocalDate und Period“

  1. Wie viele Testfälle braucht ein vollständiger Zustandsübergangstest für den Buchungsstatus, und welchen Vorteil hat der switch-Ausdruck?

Bei vier Zuständen 16 Kombinationen aus Ausgangs- und Zielzustand, von denen vier erlaubt sind. Der switch-Ausdruck über die Aufzählung wird vom Compiler auf Vollständigkeit geprüft; ein neuer Status muss dort ergänzt werden. siehe Abschnitt „Regel 6: Statuswechsel mit switch-Ausdruck“


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: “Fachlogik in reinem Java: Komponententests für Kostenbeitrag, Einwilligung und Buchungsstatus” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/spring-boot-fachlogik-komponententest 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: