SemaTrain Logo Ein Fachportal von SemaTrain

Fehlerbehandlung, Logging & Unit Testing (JUnit)

Sie lernen, Exceptions sauber zu entwerfen, Logging praxisnah einzusetzen und mit JUnit 5 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.

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 5): AAA + assertThrows

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

JUnit 5: 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 5: 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 5 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

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 5 (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.