SemaTrain Logo Ein Fachportal des SemaTrain Trainingsinstituts

Fehlerbehandlung, Logging & Unit Testing (JUnit)

Sie lernen, Exceptions sauber zu entwerfen, Logging praxisnah einzusetzen und mit JUnit 6 robuste Tests zu schreiben – so, dass Fehler schneller gefunden und Releases sicherer werden.

Hinweis: Logging-Beispiele sind bewusst „vendor-neutral“ (java.util.logging). In echten Projekten ist oft SLF4J + Logback/Log4j2 üblich. Versionsrahmen der Testbeispiele: JUnit 6.1.3 (Jupiter).

Java Fortgeschrittenen Schulung – Kursbezug

Dieses Kapitel ist Teil des Lernpfads zur Java Fortgeschrittenen Schulung. Fokus: Fehler sichtbar machen (Exceptions/Logs) und fehlerarme Releases (Tests) – im SemaTrain-Trainingskontext.

Ziel dieses Kapitels: Sie modellieren Fehler sauber, loggen verständlich (ohne Spam) und bauen einen JUnit-Check, der Regressionen verhindert.

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

Exceptions: sauber modellieren

SemaTrain-Beispiel: Buchung mit verständlicher Domain-Exception.

Domain-Exception: klarer Typ + Message (+ optional Cause) (Java)
public class BuchungException extends RuntimeException {
  public BuchungException(String message) { super(message); }
  public BuchungException(String message, Throwable cause) { super(message, cause); }
}

public class BuchungService {

  public void bucheTeilnahme(String teilnehmerId) {
    if (teilnehmerId == null || teilnehmerId.isBlank()) {
      throw new BuchungException("teilnehmerId fehlt");
    }
    // ... Buchungslogik
  }
}

Logging: Level + Kontext

Beispiel mit java.util.logging: Kontext in der Message, Fehler als Throwable übergeben.

Logging: INFO für Flow, SEVERE für Fehler (mit Throwable + Kontext) (Java)
import java.util.logging.*;

public class BuchungService {
  private static final Logger LOG = Logger.getLogger(BuchungService.class.getName());

  public void bucheTeilnahme(String teilnehmerId, String kursTitel) {
    LOG.info(() -> "Start Buchung | teilnehmerId=" + teilnehmerId + " | kurs=" + kursTitel);

    try {
      if (teilnehmerId == null || teilnehmerId.isBlank()) {
        throw new IllegalArgumentException("teilnehmerId fehlt");
      }
      if (kursTitel == null || kursTitel.isBlank()) {
        throw new IllegalArgumentException("kursTitel fehlt");
      }

      // ... business logic (z.B. Platz reservieren)
      LOG.info(() -> "Buchung ok | teilnehmerId=" + teilnehmerId + " | kurs=" + kursTitel);

    } catch (RuntimeException ex) {
      LOG.log(Level.SEVERE,
        "Buchung fehlgeschlagen | teilnehmerId=" + teilnehmerId + " | kurs=" + kursTitel,
        ex
      );
      throw ex;
    }
  }
}

Unit Testing (JUnit 6): AAA + assertThrows

Regel: Tests prüfen Verhalten/Ergebnis – nicht „wie“ es implementiert ist.

JUnit 6: AAA + assertThrows (Java)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class BuchungServiceTest {

  @Test
  void bucheTeilnahme_wirftFehler_wennTeilnehmerIdFehlt() {
    // Arrange
    BuchungService svc = new BuchungService();

    // Act + Assert
    assertThrows(IllegalArgumentException.class,
      () -> svc.bucheTeilnahme(" ", "Java Fortgeschritten")
    );
  }
}

Parametrized Tests: mehrere Inputs, ein Test

Wenn gleiche Regel für mehrere Eingaben gilt, sind Parametrized Tests ideal.

JUnit 6: ParametrizedTest (Java)
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class ValidierungTest {

  @ParameterizedTest
  @ValueSource(strings = {"", " ", "\t"})
  void teilnehmerId_ungueltig(String bad) {
    BuchungService svc = new BuchungService();
    assertThrows(IllegalArgumentException.class,
      () -> svc.bucheTeilnahme(bad, "Java Fortgeschritten")
    );
  }
}

Praxisaufgabe (Mini)

Beitrag zu den Lehr-/Lernzielen: LZ1 (Exception-Design) · LZ2 (Logging) · LZ3 (JUnit)

  1. Bauen Sie PreisRechner mit double endpreis(double basispreis, double rabatt).
  2. Wenn basispreis < 0 oder rabatt außerhalb 0..1 liegt: IllegalArgumentException mit sinnvoller Message werfen.
  3. Loggen Sie im Fehlerfall eine SEVERE-Zeile mit Kontext (basispreis/rabatt) + Throwable.
  4. Schreiben Sie 2 JUnit-Tests: 1x „Happy Path“, 1x assertThrows.
Lösung anzeigen
Lösung: PreisRechner (Validierung + Logging) (Java)
import java.util.logging.*;

public class PreisRechner {
  private static final Logger LOG = Logger.getLogger(PreisRechner.class.getName());

  public double endpreis(double basispreis, double rabatt) {
    try {
      if (basispreis < 0) throw new IllegalArgumentException("basispreis muss >= 0 sein");
      if (rabatt < 0 || rabatt > 1) throw new IllegalArgumentException("rabatt muss in 0..1 liegen");

      return basispreis * (1 - rabatt);

    } catch (RuntimeException ex) {
      LOG.log(Level.SEVERE,
        "endpreis fehlgeschlagen | basispreis=" + basispreis + " | rabatt=" + rabatt,
        ex
      );
      throw ex;
    }
  }
}
Lösung: JUnit 6 Tests (Happy + Fehlerfall) (Java)
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class PreisRechnerTest {

  @Test
  void endpreis_ok() {
    PreisRechner r = new PreisRechner();
    assertEquals(90.0, r.endpreis(100.0, 0.10), 0.0001);
  }

  @Test
  void endpreis_wirftFehler_wennRabattUngueltig() {
    PreisRechner r = new PreisRechner();
    assertThrows(IllegalArgumentException.class, () -> r.endpreis(100.0, 1.5));
  }
}
Optional: typische Stolperfallen
  • Zu generische Exceptions: lieber konkret (IllegalArgumentException, Domain-Exception).
  • printStackTrace() in Produktion: ersetzt durch Logging (mit Throwable).
  • Tests prüfen Interna: lieber Ergebnis/Fehlerverhalten prüfen.

Kurz-Takeaways

Primärreferenzen

Geprüfte Java-Referenzen

Die fachlichen Aussagen dieser Lektion werden gegen zuständige Primärreferenzen geprüft. Erklärungen, Beispiele, Aufgaben, Quizfragen und Lösungen sind eigenständige SemaTrain-Inhalte.

Lektionsprofil: JUnit, Logging und Exception-Design

Kursbasis: JDK 25 LTS

Feature-Release zum Prüfdatum: Java SE / JDK 27

Quellenprüfung: 2026-09-17

Java Platform, Standard Edition & JDK Specifications Version 25

Quellentyp: Primärspezifikation

Verantwortliche Referenzstelle: Oracle – Java SE

Versionsbezug: Java SE 25 / JDK 25

Geltungsbereich: Java-Sprache, JVM, Java-SE-Plattform und JDK-Spezifikationen.

Offizielle Primärreferenz

JDK 25 Documentation

Quellentyp: Herstellerdokumentation

Verantwortliche Referenzstelle: Oracle

Versionsbezug: JDK 25

Geltungsbereich: Offizielle JDK-API, Werkzeuge und Plattformfunktionen.

Offizielle Primärreferenz

Oracle Java SE Support Roadmap

Quellentyp: Hersteller-Roadmap

Verantwortliche Referenzstelle: Oracle

Versionsbezug: Prüfstand 17.09.2026

Geltungsbereich: Einordnung von LTS- und Feature-Releases.

Offizielle Primärreferenz

Java Language and Virtual Machine Specifications

Quellentyp: Primärspezifikation

Verantwortliche Referenzstelle: Oracle – Java SE

Versionsbezug: Java SE 27 / 26 / 25

Geltungsbereich: Offizieller Versionsindex der Java-Sprach- und JVM-Spezifikationen.

Offizielle Primärreferenz

JUnit User Guide 6.1.3

Quellentyp: Projekt-Primärdokumentation

Verantwortliche Referenzstelle: JUnit Project / JUnit Team

Versionsbezug: JUnit 6.1.3

Geltungsbereich: JUnit Platform, Jupiter, Assertions und parametrisierte Tests.

Offizielle Primärreferenz

JUnit 6.1.3 API

Quellentyp: Projekt-API-Referenz

Verantwortliche Referenzstelle: JUnit Project / JUnit Team

Versionsbezug: JUnit 6.1.3

Geltungsbereich: Offizielle API-Referenz der JUnit-Module.

Offizielle Primärreferenz

java.util.logging Package

Quellentyp: API-Primärreferenz

Verantwortliche Referenzstelle: Oracle – Java SE

Versionsbezug: JDK 25

Geltungsbereich: Logger, Level und Logging-API der Java-Plattform.

Offizielle Primärreferenz

JLS 25 – Chapter 11: Exceptions

Quellentyp: Primärspezifikation

Verantwortliche Referenzstelle: Oracle – Java SE

Versionsbezug: Java SE 25

Geltungsbereich: Java-Exception-Modell.

Offizielle Primärreferenz

Java Quellen und Methodik

Quiz: Fehlerbehandlung, Logging & Unit Testing (Lehr-/Lernziele Check)

1. (LZ1) Was gehört zu einer „guten“ Exception im Code am ehesten?

2. (LZ2) Was ist bei Logging im Fehlerfall am sinnvollsten?

3. (LZ3) Wofür nutzt man in JUnit am besten assertThrows?

4. (LZ3) Was beschreibt AAA (Arrange–Act–Assert) am besten?

Praxisaufgabe

Mini-Projekt: „Validierung + Test-Gate“ (Qualitätssicherung im SemaTrain-Kurskontext)

Beitrag zu den Lehr-/Lernzielen: LZ1 (Exception-Design) · LZ2 (Logging) · LZ3 (JUnit)

Szenario: Für Trainingsbuchungen soll es keine stillen Fehler geben. Eingaben werden validiert, Fehler werden mit Kontext geloggt und ein JUnit-Testpaket wirkt als „Gate“ vor dem Merge (Regressionen werden früh gestoppt).

Lösung anzeigen
Teil 1: Modell (BuchungAnfrage) (Java)
// 1) Modell (einfach gehalten)
public record BuchungAnfrage(
  String titel,
  int tage,
  double basispreis,
  double rabatt,   // 0..1
  String kundenId
) {}
Teil 2: Domain-Exception (Nachricht + optionale Ursache) (Java)
// 2) Domain-Exception (LZ1)
public class BuchungException extends RuntimeException {
  public BuchungException(String nachricht) { super(nachricht); }
  public BuchungException(String nachricht, Throwable ursache) { super(nachricht, ursache); }
}
Teil 3: Service (Validierung + Logging + Exception-Design) (Java)
import java.util.logging.*;

public class BuchungService {
  private static final Logger LOG = Logger.getLogger(BuchungService.class.getName());

  public double endpreis(BuchungAnfrage anfrage) {
    LOG.info(() -> "Start Buchung | titel=" + sicher(anfrage == null ? null : anfrage.titel())
      + " | kundenId=" + sicher(anfrage == null ? null : anfrage.kundenId()));

    try {
      validiere(anfrage);
      double preis = anfrage.basispreis() * (1 - anfrage.rabatt());
      LOG.info(() -> "Preis berechnet | titel=" + sicher(anfrage.titel()) + " | endpreis=" + preis);
      return preis;

    } catch (RuntimeException ex) {
      String titel   = (anfrage == null) ? null : anfrage.titel();
      String kunden  = (anfrage == null) ? null : anfrage.kundenId();
      int tage       = (anfrage == null) ? 0    : anfrage.tage();
      double basis   = (anfrage == null) ? 0.0  : anfrage.basispreis();
      double rabatt  = (anfrage == null) ? 0.0  : anfrage.rabatt();

      LOG.log(Level.SEVERE,
        "Buchung fehlgeschlagen | titel=" + sicher(titel)
          + " | tage=" + tage
          + " | basispreis=" + basis
          + " | rabatt=" + rabatt
          + " | kundenId=" + sicher(kunden),
        ex
      );
      throw ex;
    }
  }

  private void validiere(BuchungAnfrage anfrage) {
    if (anfrage == null) throw new BuchungException("anfrage ist null");
    if (anfrage.titel() == null || anfrage.titel().isBlank())
      throw new IllegalArgumentException("titel fehlt");
    if (anfrage.kundenId() == null || anfrage.kundenId().isBlank())
      throw new IllegalArgumentException("kundenId fehlt");
    if (anfrage.tage() <= 0)
      throw new IllegalArgumentException("tage muss > 0 sein");
    if (anfrage.basispreis() < 0)
      throw new IllegalArgumentException("basispreis muss >= 0 sein");
    if (anfrage.rabatt() < 0 || anfrage.rabatt() > 1)
      throw new IllegalArgumentException("rabatt muss in 0..1 liegen");
  }

  private static String sicher(String s) {
    return (s == null) ? "(null)" : s;
  }
}
Teil 4: JUnit 6 (Happy Path + assertThrows + ParametrizedTest) (Java)
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;

class BuchungServiceTest {

  @Test
  void endpreis_ok_happyPath() {
    BuchungService svc = new BuchungService();
    BuchungAnfrage anfrage = new BuchungAnfrage(
      "Java Fortgeschritten (SemaTrain)", 5, 2082.50, 0.10, "kunde-123"
    );
    double preis = svc.endpreis(anfrage);
    assertEquals(1874.25, preis, 0.001);
  }

  @Test
  void endpreis_wirftFehler_wennRabattUngueltig() {
    BuchungService svc = new BuchungService();
    BuchungAnfrage anfrage = new BuchungAnfrage(
      "Java Fortgeschritten", 5, 100.0, 1.5, "kunde-123"
    );
    assertThrows(IllegalArgumentException.class, () -> svc.endpreis(anfrage));
  }

  @ParameterizedTest
  @ValueSource(strings = {"", " ", "\t"})
  void endpreis_wirftFehler_wennKundenIdLeer(String schlechteKundenId) {
    BuchungService svc = new BuchungService();
    BuchungAnfrage anfrage = new BuchungAnfrage(
      "Java Fortgeschritten", 5, 100.0, 0.10, schlechteKundenId
    );
    assertThrows(IllegalArgumentException.class, () -> svc.endpreis(anfrage));
  }
}
Teil 5: Test-Gate in CI/CD (Merge-Blocker) (Text)
// CI-Idee (Text):
// - In der CI (z.B. GitHub Actions, Jenkins) wird bei jedem Push ausgeführt:
//     mvn test   oder   gradle test
// - Wenn Tests fehlschlagen, wird der Merge blockiert (Branch Protection / Build-Gate).
// Ergebnis: Kaputtes Verhalten (Preislogik/Validierung) kommt nicht ins Release.

SemaTrain Trainingsinstitut

Java praktisch vertiefen

Diese Wissenslektion ist Bestandteil des SemaTrain-Java-Lernpfads. Das begleitete Training ergänzt Wissen durch Programmierpraxis, Feedback und Transfer.

Zum Java Trainingscenter Quellen und Methodik