50 Testing-Labs

Testing Refactoring Workbench

Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.

Fachlicher Lernpfad

Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen

Lernzielorientierter Zugang

Testing als Sicherheitsnetz des Refactorings

Die vorhandenen Kapitel bleiben vollständig erhalten. Diese Orientierung gruppiert sie nach den Entscheidungen, die ein Enterprise-Team tatsächlich treffen muss.

Verhalten sichern

Characterization Tests und Golden Master vor strukturellen Änderungen einsetzen.

Fachregeln präzisieren

Decision Tables, Parameterisierung und Value-Object-Tests für Invarianten nutzen.

Grenzen isolieren

Fakes, Stubs und Contract Tests an Ports statt an Implementierungsdetails ausrichten.

Schrittweise prüfen

Nach jedem Refactoring-Schritt einen kleinen, aussagekräftigen Testlauf ermöglichen.

Kapitel 1 Characterization Tests und Golden Master5 Praxislabore

Fünf grundlegende Testrefactorings zeigen, wie Tests Verhalten absichern, Refactorings ermöglichen und nicht an Implementierungsdetails koppeln.

Lab 1 Characterization Test Boundary Zuerst beobachtbares Verhalten einfrieren, danach intern zerlegen und jeden Schritt gegen den Golden Master prüfen.

Characterization Test Boundary

Code Smell

Legacy-Berechnung wird refaktoriert, obwohl ihr tatsächliches Verhalten nicht abgesichert ist.

Pattern / Prinzip

Characterization Test + Golden Master

Testfokus

Stabile Beobachtungspunkte, Randfälle und absichtliche Altfehler dokumentieren.

Alternative

Direkte Unit Tests, wenn fachliche Regeln bereits eindeutig dokumentiert sind.

Vor dem Refactoring

fragiles Testdesign
// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.

Referenzlösung

CharacterizationTestBoundary.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;

// Testing Pattern: Characterization Test + Golden Master
// Zweck: Unbekanntes Legacy-Verhalten vor strukturellem Refactoring absichern.
public final class CharacterizationTestBoundary {
  public record Line(BigDecimal net, int quantity, boolean reducedTax) {
    public Line { Objects.requireNonNull(net); if (quantity < 1) throw new IllegalArgumentException("quantity"); }
  }
  public record Snapshot(BigDecimal gross, BigDecimal tax, int lineCount) {}

  public Snapshot observe(List<Line> lines) {
    Objects.requireNonNull(lines);
    BigDecimal net = lines.stream()
        .map(line -> line.net().multiply(BigDecimal.valueOf(line.quantity())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    BigDecimal tax = lines.stream()
        .map(line -> line.net().multiply(BigDecimal.valueOf(line.quantity()))
            .multiply(line.reducedTax() ? new BigDecimal("0.10") : new BigDecimal("0.20")))
        .reduce(BigDecimal.ZERO, BigDecimal::add)
        .setScale(2, RoundingMode.HALF_UP);
    return new Snapshot(net.add(tax).setScale(2, RoundingMode.HALF_UP), tax, lines.size());
  }
}
Refactoring-Regel: Zuerst beobachtbares Verhalten einfrieren, danach intern zerlegen und jeden Schritt gegen den Golden Master prüfen.

Lab 2 Test Data Builder Gültige Defaults in einem Builder bündeln; pro Test nur fachlich relevante Abweichungen sichtbar machen.

Test Data Builder

Code Smell

Tests erzeugen große Fachobjekte mit unlesbaren Konstruktoren und zufälligen Standardwerten.

Pattern / Prinzip

Builder + Object Mother Boundary

Testfokus

Builder-Defaults müssen gültig, deterministisch und fachlich neutral sein.

Alternative

Factory Methods für wenige klar benannte Standardszenarien.

Vor dem Refactoring

fragiles Testdesign
// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.

Referenzlösung

OrderTestDataBuilder.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Objects;

// Testing Pattern: Test Data Builder
// Zweck: Fachlich relevante Testabweichungen sichtbar machen, ohne ungültige Fixtures zu erzeugen.
public final class OrderTestDataBuilder {
  public record Order(String id, String customerId, List<BigDecimal> lines, Instant createdAt, Status status) {
    public Order {
      Objects.requireNonNull(id); Objects.requireNonNull(customerId);
      lines = List.copyOf(lines); Objects.requireNonNull(createdAt); Objects.requireNonNull(status);
      if (lines.isEmpty()) throw new IllegalArgumentException("lines");
    }
    public BigDecimal total() { return lines.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
  }
  public enum Status { DRAFT, CONFIRMED }

  private String id = "order-100";
  private String customerId = "customer-10";
  private List<BigDecimal> lines = List.of(new BigDecimal("49.90"));
  private Instant createdAt = Instant.parse("2026-01-15T10:00:00Z");
  private Status status = Status.DRAFT;

  public OrderTestDataBuilder withLines(BigDecimal... values) { lines = List.of(values); return this; }
  public OrderTestDataBuilder confirmed() { status = Status.CONFIRMED; return this; }
  public OrderTestDataBuilder forCustomer(String value) { customerId = value; return this; }
  public Order build() { return new Order(id, customerId, lines, createdAt, status); }
}
Refactoring-Regel: Gültige Defaults in einem Builder bündeln; pro Test nur fachlich relevante Abweichungen sichtbar machen.

Lab 3 Decision Table Parameterization Eingaben und erwartete Entscheidungen als explizite Fälle modellieren; eine Testschleife validiert die Tabelle.

Decision Table Parameterization

Code Smell

Dieselbe fachliche Regel wird in vielen fast identischen Tests kopiert und driftet auseinander.

Pattern / Prinzip

Decision Table + Parameterized Test

Testfokus

Fallnamen, Grenzwerte und erwartete Begründungen sind Teil des Testvertrags.

Alternative

Einzeltests, wenn jeder Fall einen deutlich anderen Ablauf oder eine eigene Erklärung benötigt.

Vor dem Refactoring

fragiles Testdesign
// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.

Referenzlösung

DiscountDecisionTable.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.util.List;

// Testing Pattern: Decision Table + Parameterized Test
// Zweck: Fachliche Kombinationen vollständig und ohne Testduplikation beschreiben.
public final class DiscountDecisionTable {
  public enum Segment { STANDARD, BUSINESS, VIP }
  public record Case(String name, Segment segment, BigDecimal total, int expectedPercent) {}

  public int discountPercent(Segment segment, BigDecimal total) {
    if (segment == Segment.VIP && total.compareTo(new BigDecimal("1000")) >= 0) return 15;
    if (segment == Segment.BUSINESS && total.compareTo(new BigDecimal("500")) >= 0) return 8;
    if (segment == Segment.VIP) return 5;
    return 0;
  }

  public List<Case> contractCases() {
    return List.of(
        new Case("standard-no-discount", Segment.STANDARD, new BigDecimal("999"), 0),
        new Case("vip-base", Segment.VIP, new BigDecimal("999.99"), 5),
        new Case("vip-threshold", Segment.VIP, new BigDecimal("1000"), 15),
        new Case("business-threshold", Segment.BUSINESS, new BigDecimal("500"), 8));
  }
}
Refactoring-Regel: Eingaben und erwartete Entscheidungen als explizite Fälle modellieren; eine Testschleife validiert die Tabelle.

Lab 4 In-Memory Fake Repository Einen zustandsbehafteten Fake gegen denselben Repository-Vertrag einsetzen; Assertions prüfen sichtbare Ergebnisse.

In-Memory Fake Repository

Code Smell

Übermockte Tests prüfen Interaktionsdetails statt fachliches Verhalten und brechen bei jeder Umstrukturierung.

Pattern / Prinzip

Fake + Repository Contract

Testfokus

Fake und Produktionsadapter müssen denselben Contract-Test bestehen.

Alternative

Mock für echte Protokollgrenzen, wenn Reihenfolge oder exakt ein Aufruf Vertragsbestandteil ist.

Vor dem Refactoring

fragiles Testdesign
// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.

Referenzlösung

CustomerRepositoryFake.java
package com.aydinsude.workbench.testing;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

interface CustomerRepository {
  void save(CustomerRepositoryFake.Customer customer);
  Optional<CustomerRepositoryFake.Customer> findById(String id);
  long countActive();
}

// Testing Pattern: In-Memory Fake + Repository Contract
// Zweck: Zustand und fachliches Ergebnis testen, ohne Implementierungsdetails zu mocken.
public final class CustomerRepositoryFake implements CustomerRepository {
  public record Customer(String id, String email, boolean active) {}
  private final Map<String, Customer> storage = new LinkedHashMap<>();
  @Override public void save(Customer customer) { storage.put(customer.id(), customer); }
  @Override public Optional<Customer> findById(String id) { return Optional.ofNullable(storage.get(id)); }
  @Override public long countActive() { return storage.values().stream().filter(Customer::active).count(); }
}
Refactoring-Regel: Einen zustandsbehafteten Fake gegen denselben Repository-Vertrag einsetzen; Assertions prüfen sichtbare Ergebnisse.

Lab 5 Reusable Contract Test Kit Port-Invarianten als wiederverwendbaren Contract formulieren; jeder Adapter liefert nur seine Fixture.

Reusable Contract Test Kit

Code Smell

Mehrere Adapter behaupten denselben Port zu implementieren, werden aber mit unterschiedlichen Erwartungen getestet.

Pattern / Prinzip

Contract Test + Test Template

Testfokus

Vertrag testet beobachtbares Verhalten, nicht interne Datenstrukturen des Adapters.

Alternative

End-to-End-Test, wenn die Aussage nur im vollständig integrierten System möglich ist.

Vor dem Refactoring

fragiles Testdesign
// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.

Referenzlösung

PaymentGatewayContractKit.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;

// Testing Pattern: Contract Test Kit
// Zweck: Alle Adapter eines Ports gegen identische Invarianten prüfen.
public final class PaymentGatewayContractKit {
  public record Request(String idempotencyKey, BigDecimal amount) {}
  public record Result(String providerId, Status status) {}
  public enum Status { ACCEPTED, REJECTED }
  public interface Gateway { Result charge(Request request); }

  public static final class DeterministicGateway implements Gateway {
    private final Map<String, Result> processed = new LinkedHashMap<>();
    @Override public Result charge(Request request) {
      if (request.amount().signum() <= 0) return new Result("none", Status.REJECTED);
      return processed.computeIfAbsent(request.idempotencyKey(),
          key -> new Result("pay-" + (processed.size() + 1), Status.ACCEPTED));
    }
  }

  public static void verify(Gateway gateway) {
    var request = new Request("order-7", new BigDecimal("19.90"));
    var first = gateway.charge(request);
    var repeated = gateway.charge(request);
    if (first.status() != Status.ACCEPTED) throw new AssertionError("positive amount must be accepted");
    if (!first.equals(repeated)) throw new AssertionError("same key must be idempotent");
  }
}
Refactoring-Regel: Port-Invarianten als wiederverwendbaren Contract formulieren; jeder Adapter liefert nur seine Fixture.
Kapitel 2 Deterministische Zeit und kontrollierte Abhängigkeiten5 Praxislabore

Fünf Labs entfernen Zeit-, Identitäts-, Ausgangs- und Nebenläufigkeits-Zufall aus Tests und erweitern Beispieltests zu reproduzierbaren Invarianten.

Lab 6 Deterministic Clock Boundary Zeit als Abhängigkeit behandeln; Produktion liefert Systemzeit, Tests einen festen Clock.

Deterministic Clock Boundary

Code Smell

Tests verwenden Instant.now() oder LocalDate.now() direkt und werden zeitabhängig, langsam oder flakey.

Pattern / Prinzip

Clock Port + Dependency Injection

Testfokus

Grenzzeitpunkte, Zeitzonen und exakt reproduzierbare Ablaufentscheidungen.

Alternative

Feste Zeitparameter direkt an reine Funktionen übergeben, wenn kein laufender Zeitbezug nötig ist.

Vor dem Refactoring

fragiles Testdesign
// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.

Referenzlösung

DeterministicClockBoundary.java
package com.aydinsude.workbench.testing;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;

// Testing Pattern: Clock Port + Dependency Injection
// Zweck: Zeitabhaengige Regeln ohne Sleeps und ohne Systemzeit testen.
public final class DeterministicClockBoundary {
  private final Clock clock;
  private final Duration validity;

  public DeterministicClockBoundary(Clock clock, Duration validity) {
    this.clock = Objects.requireNonNull(clock);
    this.validity = Objects.requireNonNull(validity);
  }

  public boolean isExpired(Instant createdAt) {
    return !createdAt.plus(validity).isAfter(clock.instant());
  }
}

JUnit-Test

DeterministicClockBoundaryTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.time.*;
import org.junit.jupiter.api.Test;

class DeterministicClockBoundaryTest {
  private final Instant now = Instant.parse("2026-07-10T12:00:00Z");
  private final Clock fixed = Clock.fixed(now, ZoneOffset.UTC);
  private final DeterministicClockBoundary policy =
      new DeterministicClockBoundary(fixed, Duration.ofMinutes(30));

  @Test void expires_exactly_at_the_boundary() {
    assertTrue(policy.isExpired(now.minus(Duration.ofMinutes(30))));
  }

  @Test void remains_valid_one_second_before_boundary() {
    assertFalse(policy.isExpired(now.minus(Duration.ofMinutes(29).plusSeconds(59))));
  }
}
Refactoring-Regel: Zeit als Abhängigkeit behandeln; Produktion liefert Systemzeit, Tests einen festen Clock.

Lab 7 Deterministic ID Generator Nichtdeterministische Identität hinter einen Port verschieben und im Test eine Sequenz vorgeben.

Deterministic ID Generator

Code Smell

UUID.randomUUID() steckt mitten im Use Case; Assertions kennen nur Formate, nicht die erzeugte Identität.

Pattern / Prinzip

Generator Port + Deterministic Stub

Testfokus

Exakte Zuordnung zwischen erzeugtem Aggregat, Event und Persistenzschlüssel.

Alternative

Wert aus dem Request übernehmen, wenn die Identität fachlich vom Aufrufer kommt.

Vor dem Refactoring

fragiles Testdesign
// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.

Referenzlösung

DeterministicIdGenerator.java
package com.aydinsude.workbench.testing;

import java.util.Objects;
import java.util.function.Supplier;

// Testing Pattern: Generator Port + Deterministic Stub
// Zweck: Identitaet reproduzierbar und ueber mehrere Ausgaben konsistent pruefen.
public final class DeterministicIdGenerator {
  public record Ticket(String id, String subject) {}
  public record TicketCreated(String ticketId) {}
  public record Result(Ticket ticket, TicketCreated event) {}

  private final Supplier<String> ids;
  public DeterministicIdGenerator(Supplier<String> ids) {
    this.ids = Objects.requireNonNull(ids);
  }

  public Result create(String subject) {
    String id = ids.get();
    return new Result(new Ticket(id, subject), new TicketCreated(id));
  }
}

JUnit-Test

DeterministicIdGeneratorTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class DeterministicIdGeneratorTest {
  @Test void uses_the_same_generated_id_for_aggregate_and_event() {
    var useCase = new DeterministicIdGenerator(() -> "ticket-42");
    var result = useCase.create("Payment failed");
    assertAll(
      () -> assertEquals("ticket-42", result.ticket().id()),
      () -> assertEquals(result.ticket().id(), result.event().ticketId())
    );
  }
}
Refactoring-Regel: Nichtdeterministische Identität hinter einen Port verschieben und im Test eine Sequenz vorgeben.

Lab 8 Output Capture Port Seiteneffekte über einen Port ausgeben; Recording Fake sammelt fachlich sichtbare Nachrichten.

Output Capture Port

Code Smell

Tests greifen auf statische Logger, Mail-Singletons oder globale Listen zu und beeinflussen sich gegenseitig.

Pattern / Prinzip

Output Port + Recording Fake

Testfokus

Inhalt, Reihenfolge und Empfänger sichtbarer Ausgaben ohne globale Zustände.

Alternative

Mock verwenden, wenn exakt ein Protokollaufruf selbst Vertragsbestandteil ist.

Vor dem Refactoring

fragiles Testdesign
// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.

Referenzlösung

OutputCapturePort.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

interface NotificationOutput {
  void send(OutputCapturePort.Message message);
}

// Testing Pattern: Output Port + Recording Fake
// Zweck: Seiteneffekte als beobachtbare Ausgaben ohne globale Testzustände pruefen.
public final class OutputCapturePort {
  public record Message(String recipient, String subject, String body) {}

  public static final class RecordingOutput implements NotificationOutput {
    private final List<Message> messages = new ArrayList<>();
    @Override public void send(Message message) { messages.add(Objects.requireNonNull(message)); }
    public List<Message> messages() { return List.copyOf(messages); }
  }

  private final NotificationOutput output;
  public OutputCapturePort(NotificationOutput output) { this.output = Objects.requireNonNull(output); }
  public void notifyApproval(String email, String caseId) {
    output.send(new Message(email, "Case approved", "Case " + caseId + " was approved."));
  }
}

JUnit-Test

OutputCapturePortTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class OutputCapturePortTest {
  @Test void exposes_the_business_message_without_static_state() {
    var output = new OutputCapturePort.RecordingOutput();
    new OutputCapturePort(output).notifyApproval("a@example.test", "case-7");
    assertEquals(
      new OutputCapturePort.Message("a@example.test", "Case approved", "Case case-7 was approved."),
      output.messages().getFirst()
    );
  }
}
Refactoring-Regel: Seiteneffekte über einen Port ausgeben; Recording Fake sammelt fachlich sichtbare Nachrichten.

Lab 9 Eventually Probe without Sleeps Auf eine fachliche Bedingung pollen; niemals feste Sleeps als Synchronisationsvertrag verwenden.

Eventually Probe without Sleeps

Code Smell

Asynchrone Tests schlafen pauschal mehrere Sekunden und sind langsam oder auf belasteten Systemen instabil.

Pattern / Prinzip

Polling Probe + Time Budget

Testfokus

Ergebnis innerhalb eines Budgets, aussagekräftige letzte Beobachtung und sofortiger Erfolg.

Alternative

CompletionStage direkt awaiten, wenn der Produktionsvertrag bereits einen Abschluss-Handle liefert.

Vor dem Refactoring

fragiles Testdesign
// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.

Referenzlösung

EventuallyProbe.java
package com.aydinsude.workbench.testing;

import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.function.BooleanSupplier;

// Testing Pattern: Polling Probe + Time Budget
// Zweck: Asynchrone Ergebnisse ohne starre Sleep-Zeiten beobachten.
public final class EventuallyProbe {
  public interface NanoClock { long nanoTime(); }
  public interface Pause { void pause(Duration duration) throws InterruptedException; }

  private final NanoClock clock;
  private final Pause pause;
  public EventuallyProbe(NanoClock clock, Pause pause) {
    this.clock = Objects.requireNonNull(clock);
    this.pause = Objects.requireNonNull(pause);
  }

  public void until(BooleanSupplier condition, Duration timeout, Duration interval) {
    long deadline = clock.nanoTime() + timeout.toNanos();
    while (!condition.getAsBoolean()) {
      if (clock.nanoTime() >= deadline) throw new AssertionError("condition not met within " + timeout);
      try { pause.pause(interval); }
      catch (InterruptedException ex) { Thread.currentThread().interrupt(); throw new AssertionError(ex); }
    }
  }
}

JUnit-Test

EventuallyProbeTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class EventuallyProbeTest {
  @Test void stops_immediately_when_condition_becomes_true() {
    var nanos = new AtomicInteger();
    var checks = new AtomicInteger();
    var probe = new EventuallyProbe(nanos::get, d -> nanos.addAndGet((int)d.toNanos()));
    probe.until(() -> checks.incrementAndGet() >= 3, Duration.ofNanos(10), Duration.ofNanos(1));
    assertEquals(3, checks.get());
  }

  @Test void fails_when_budget_is_exhausted() {
    var nanos = new AtomicInteger();
    var probe = new EventuallyProbe(nanos::get, d -> nanos.addAndGet((int)d.toNanos()));
    assertThrows(AssertionError.class,
      () -> probe.until(() -> false, Duration.ofNanos(2), Duration.ofNanos(1)));
  }
}
Refactoring-Regel: Auf eine fachliche Bedingung pollen; niemals feste Sleeps als Synchronisationsvertrag verwenden.

Lab 10 Invariant Test Harness Nicht einzelne Ergebnisse erraten, sondern dauerhafte Fachinvarianten über viele reproduzierbare Fälle prüfen.

Invariant Test Harness

Code Smell

Beispieltests prüfen wenige bekannte Werte, während ganze Klassen gültiger Eingaben ungeschützt bleiben.

Pattern / Prinzip

Property/Invariant Harness + Deterministic Seed

Testfokus

Algebraische Invarianten, Grenzwerte, reproduzierbare Zufallsfolgen und minimale Gegenbeispiele.

Alternative

Klassische Parameterized Tests, wenn die Eingabemenge klein und vollständig aufzählbar ist.

Vor dem Refactoring

fragiles Testdesign
// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.

Referenzlösung

InvariantTestHarness.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;

// Testing Pattern: Property/Invariant Harness + Deterministic Seed
// Zweck: Fachinvarianten ueber viele reproduzierbare Eingaben pruefen.
public final class InvariantTestHarness {
  public BigDecimal applyDiscount(BigDecimal amount, int percent) {
    if (amount.signum() < 0 || percent < 0 || percent > 100) throw new IllegalArgumentException();
    return amount.multiply(BigDecimal.valueOf(100L - percent))
        .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
  }

  public void verify(long seed, int cases) {
    Random random = new Random(seed);
    for (int i = 0; i < cases; i++) {
      BigDecimal amount = BigDecimal.valueOf(random.nextInt(1_000_000), 2);
      int percent = random.nextInt(101);
      BigDecimal result = applyDiscount(amount, percent);
      if (result.signum() < 0 || result.compareTo(amount) > 0) {
        throw new AssertionError("seed=" + seed + ", case=" + i + ", amount=" + amount + ", percent=" + percent);
      }
    }
  }
}

JUnit-Test

InvariantTestHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class InvariantTestHarnessTest {
  @Test void discount_never_makes_a_positive_amount_negative_or_larger() {
    assertDoesNotThrow(() -> new InvariantTestHarness().verify(20260710L, 10_000));
  }

  @Test void one_hundred_percent_discount_results_in_zero() {
    assertEquals(new BigDecimal("0.00"),
      new InvariantTestHarness().applyDiscount(new BigDecimal("19.90"), 100));
  }
}
Refactoring-Regel: Nicht einzelne Ergebnisse erraten, sondern dauerhafte Fachinvarianten über viele reproduzierbare Fälle prüfen.
Kapitel 3 Zustandsübergänge und fachliche Testmatrizen5 Praxislabore

Fünf Labs stärken den Testvertrag: vollständige Zustandsmatrizen, kanonische Golden Master, mutationsfeste Oracles, stabile Exception-Verträge und reproduzierbare Concurrency-Stresstests.

Lab 11 State Transition Test Matrix Zustandsautomaten als vollständige Übergangsmatrix testen, nicht als lose Sammlung einzelner Beispiele.

State Transition Test Matrix

Code Smell

Einzeltests prüfen nur glückliche Pfade; verbotene Statuswechsel und terminale Zustände bleiben ungeschützt.

Pattern / Prinzip

Transition Table + Parameterized Contract

Testfokus

Alle erlaubten und verbotenen Übergänge sowie unveränderte Zustände bei Ablehnung.

Alternative

Property Tests, wenn die Zustandsmaschine sehr groß oder datengetrieben ist.

Vor dem Refactoring

schwacher Testvertrag
// Der Test prüft nur einen Einzelpfad, irgendeine Exception oder eine oberflächliche Eigenschaft.
// Fachliche Grenzwerte, verbotene Übergänge oder konkurrierende Ausführung bleiben offen.

Referenzlösung

StateTransitionMatrix.java
package com.aydinsude.workbench.testing;

import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;

// Testing Pattern: Transition Table + Parameterized Contract
// Zweck: Erlaubte und verbotene Statuswechsel vollstaendig und zentral pruefbar machen.
public final class StateTransitionMatrix {
  public enum Status { CREATED, VALIDATED, APPROVED, REJECTED, CLOSED }

  private final Map<Status, Set<Status>> allowed = new EnumMap<>(Status.class);

  public StateTransitionMatrix() {
    allowed.put(Status.CREATED, EnumSet.of(Status.VALIDATED, Status.REJECTED));
    allowed.put(Status.VALIDATED, EnumSet.of(Status.APPROVED, Status.REJECTED));
    allowed.put(Status.APPROVED, EnumSet.of(Status.CLOSED));
    allowed.put(Status.REJECTED, EnumSet.of(Status.CLOSED));
    allowed.put(Status.CLOSED, EnumSet.noneOf(Status.class));
  }

  public Status transition(Status current, Status target) {
    if (!allowed.get(current).contains(target)) {
      throw new IllegalStateException("transition " + current + " -> " + target + " is not allowed");
    }
    return target;
  }

  public boolean isAllowed(Status current, Status target) {
    return allowed.get(current).contains(target);
  }
}

JUnit-Test

StateTransitionMatrixTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class StateTransitionMatrixTest {
  private final StateTransitionMatrix matrix = new StateTransitionMatrix();

  static Stream<Arguments> allowedTransitions() {
    return Stream.of(
      Arguments.of(StateTransitionMatrix.Status.CREATED, StateTransitionMatrix.Status.VALIDATED),
      Arguments.of(StateTransitionMatrix.Status.VALIDATED, StateTransitionMatrix.Status.APPROVED),
      Arguments.of(StateTransitionMatrix.Status.APPROVED, StateTransitionMatrix.Status.CLOSED)
    );
  }

  @ParameterizedTest @MethodSource("allowedTransitions")
  void accepts_documented_transitions(StateTransitionMatrix.Status from, StateTransitionMatrix.Status to) {
    assertEquals(to, matrix.transition(from, to));
  }

  @ParameterizedTest
  @MethodSource("forbiddenTransitions")
  void rejects_forbidden_transitions(StateTransitionMatrix.Status from, StateTransitionMatrix.Status to) {
    assertThrows(IllegalStateException.class, () -> matrix.transition(from, to));
  }

  static Stream<Arguments> forbiddenTransitions() {
    return Stream.of(
      Arguments.of(StateTransitionMatrix.Status.CREATED, StateTransitionMatrix.Status.CLOSED),
      Arguments.of(StateTransitionMatrix.Status.CLOSED, StateTransitionMatrix.Status.CREATED),
      Arguments.of(StateTransitionMatrix.Status.REJECTED, StateTransitionMatrix.Status.APPROVED)
    );
  }
}
Refactoring-Regel: Zustandsautomaten als vollständige Übergangsmatrix testen, nicht als lose Sammlung einzelner Beispiele.

Lab 12 Canonical Golden Master Golden Master nur auf kanonische, fachlich relevante Darstellung anwenden; volatile Metadaten vorher entfernen.

Canonical Golden Master

Code Smell

Große Legacy-Ausgaben werden mit dutzenden fragilen Feldassertions geprüft oder gar nicht abgesichert.

Pattern / Prinzip

Golden Master + Canonical Serializer

Testfokus

Stabile fachliche Ausgabe, deterministische Reihenfolge und bewusst sichtbare Regressionen.

Alternative

Gezielte Assertions, wenn nur wenige fachlich relevante Werte existieren.

Vor dem Refactoring

schwacher Testvertrag
// Der Test prüft nur einen Einzelpfad, irgendeine Exception oder eine oberflächliche Eigenschaft.
// Fachliche Grenzwerte, verbotene Übergänge oder konkurrierende Ausführung bleiben offen.

Referenzlösung

CanonicalGoldenMaster.java
package com.aydinsude.workbench.testing;

import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

// Testing Pattern: Golden Master + Canonical Serializer
// Zweck: Komplexe Legacy-Ausgaben stabil absichern, ohne volatile Darstellung zu speichern.
public final class CanonicalGoldenMaster {
  public record Line(String sku, int quantity, long cents) {}
  public record Invoice(String id, List<Line> lines) {
    public Invoice { lines = List.copyOf(lines); }
  }

  public String canonicalize(Invoice invoice) {
    Objects.requireNonNull(invoice);
    String lines = invoice.lines().stream()
      .sorted(Comparator.comparing(Line::sku))
      .map(l -> l.sku() + "|" + l.quantity() + "|" + l.cents())
      .collect(Collectors.joining("\n"));
    long total = invoice.lines().stream().mapToLong(l -> l.quantity() * l.cents()).sum();
    return "invoice=" + invoice.id() + "\n" + lines + "\ntotal=" + total;
  }
}

JUnit-Test

CanonicalGoldenMasterTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import org.junit.jupiter.api.Test;

class CanonicalGoldenMasterTest {
  @Test void matches_the_reviewed_business_snapshot() {
    var invoice = new CanonicalGoldenMaster.Invoice("INV-42", List.of(
      new CanonicalGoldenMaster.Line("B-2", 1, 250),
      new CanonicalGoldenMaster.Line("A-1", 2, 125)
    ));
    String approved = """
      invoice=INV-42
      A-1|2|125
      B-2|1|250
      total=500""";
    assertEquals(approved, new CanonicalGoldenMaster().canonicalize(invoice));
  }
}
Refactoring-Regel: Golden Master nur auf kanonische, fachlich relevante Darstellung anwenden; volatile Metadaten vorher entfernen.

Lab 13 Mutation-Resistant Assertions Assertions müssen fachliche Entscheidungen beweisen und typische Mutationen gezielt töten.

Mutation-Resistant Assertions

Code Smell

Tests prüfen nur not-null, Größe oder Erfolgsstatus; falsche Operatoren und vertauschte Fachwerte überleben unentdeckt.

Pattern / Prinzip

Behavioral Oracle + Rich Result

Testfokus

Fachentscheidung, Betrag, Begründung und Nebenbedingungen werden gemeinsam präzise geprüft.

Alternative

Approval Test für sehr große strukturierte Ergebnisse, ergänzt um kritische Invarianten.

Vor dem Refactoring

schwacher Testvertrag
// Der Test prüft nur einen Einzelpfad, irgendeine Exception oder eine oberflächliche Eigenschaft.
// Fachliche Grenzwerte, verbotene Übergänge oder konkurrierende Ausführung bleiben offen.

Referenzlösung

MutationResistantAssertions.java
package com.aydinsude.workbench.testing;

import java.util.Objects;

// Testing Pattern: Behavioral Oracle + Rich Result
// Zweck: Fachlich starke Assertions statt oberflaechlicher Status- oder Not-null-Pruefungen.
public final class MutationResistantAssertions {
  public record Decision(boolean approved, int score, String reason) {
    public Decision { Objects.requireNonNull(reason); }
  }

  public Decision decide(int income, int obligations) {
    if (income < 0 || obligations < 0) throw new IllegalArgumentException();
    int disposable = income - obligations;
    int score = Math.max(0, Math.min(100, disposable / 50));
    boolean approved = disposable >= 1_500 && score >= 30;
    return new Decision(approved, score, approved ? "AFFORDABLE" : "INSUFFICIENT_MARGIN");
  }
}

JUnit-Test

MutationResistantAssertionsTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class MutationResistantAssertionsTest {
  private final MutationResistantAssertions policy = new MutationResistantAssertions();

  @Test void approves_exact_boundary_with_complete_business_oracle() {
    var result = policy.decide(3_000, 1_500);
    assertAll(
      () -> assertTrue(result.approved()),
      () -> assertEquals(30, result.score()),
      () -> assertEquals("AFFORDABLE", result.reason())
    );
  }

  @Test void rejects_one_unit_below_boundary() {
    var result = policy.decide(2_999, 1_500);
    assertAll(
      () -> assertFalse(result.approved()),
      () -> assertEquals(29, result.score()),
      () -> assertEquals("INSUFFICIENT_MARGIN", result.reason())
    );
  }
}
Refactoring-Regel: Assertions müssen fachliche Entscheidungen beweisen und typische Mutationen gezielt töten.

Lab 14 Exception Contract Test Exceptions als öffentlichen Vertrag testen: Typ, Code, Kontext und Cause gehören zusammen.

Exception Contract Test

Code Smell

Tests erwarten nur irgendeine Exception; Fehlercode, Ursache und fachlicher Kontext können unbemerkt verloren gehen.

Pattern / Prinzip

Exception Translation + Contract Test

Testfokus

Stabiler Fehlercode, relevante Kontextdaten und erhaltene technische Ursache.

Alternative

Result Type für erwartbare fachliche Ablehnungen ohne Ausnahmefluss.

Vor dem Refactoring

schwacher Testvertrag
// Der Test prüft nur einen Einzelpfad, irgendeine Exception oder eine oberflächliche Eigenschaft.
// Fachliche Grenzwerte, verbotene Übergänge oder konkurrierende Ausführung bleiben offen.

Referenzlösung

ExceptionContract.java
package com.aydinsude.workbench.testing;

import java.util.Objects;

// Testing Pattern: Exception Translation + Contract Test
// Zweck: Technische Fehler in einen stabilen fachlichen Fehlervertrag uebersetzen.
public final class ExceptionContract {
  public interface CustomerGateway { String load(String customerId); }

  public static final class CustomerLookupException extends RuntimeException {
    private final String code;
    private final String customerId;
    public CustomerLookupException(String code, String customerId, Throwable cause) {
      super("customer lookup failed: " + customerId, cause);
      this.code = Objects.requireNonNull(code);
      this.customerId = Objects.requireNonNull(customerId);
    }
    public String code() { return code; }
    public String customerId() { return customerId; }
  }

  private final CustomerGateway gateway;
  public ExceptionContract(CustomerGateway gateway) { this.gateway = Objects.requireNonNull(gateway); }

  public String load(String customerId) {
    try { return gateway.load(customerId); }
    catch (RuntimeException cause) {
      throw new CustomerLookupException("CUSTOMER_SOURCE_UNAVAILABLE", customerId, cause);
    }
  }
}

JUnit-Test

ExceptionContractTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class ExceptionContractTest {
  @Test void preserves_code_context_and_original_cause() {
    var sourceFailure = new IllegalStateException("database offline");
    var service = new ExceptionContract(id -> { throw sourceFailure; });
    var error = assertThrows(ExceptionContract.CustomerLookupException.class,
      () -> service.load("customer-7"));
    assertAll(
      () -> assertEquals("CUSTOMER_SOURCE_UNAVAILABLE", error.code()),
      () -> assertEquals("customer-7", error.customerId()),
      () -> assertSame(sourceFailure, error.getCause())
    );
  }
}
Refactoring-Regel: Exceptions als öffentlichen Vertrag testen: Typ, Code, Kontext und Cause gehören zusammen.

Lab 15 Concurrency Stress Harness Nebenläufigkeit mit synchronisiertem Start und fachlicher Endinvariante prüfen, nicht mit zufälligen Sleeps.

Concurrency Stress Harness

Code Smell

Ein einziger Thread-Test bestätigt nur den Happy Path; Lost Updates und doppelte Reservierungen bleiben zufällig unsichtbar.

Pattern / Prinzip

Start Gate + Concurrent Stress Harness

Testfokus

Gleichzeitiger Start, wiederholte Ausführung und harte Invariante über atomare Ergebnisse.

Alternative

JCStress oder spezialisierte Model Checker für formale Low-Level-Concurrency-Analyse.

Vor dem Refactoring

schwacher Testvertrag
// Der Test prüft nur einen Einzelpfad, irgendeine Exception oder eine oberflächliche Eigenschaft.
// Fachliche Grenzwerte, verbotene Übergänge oder konkurrierende Ausführung bleiben offen.

Referenzlösung

ConcurrencyStressHarness.java
package com.aydinsude.workbench.testing;

import java.util.concurrent.atomic.AtomicInteger;

// Testing Pattern: Atomic State + Stress-Testable Invariant
// Zweck: Reservierungen trotz konkurrierender Aufrufer niemals ueber den Bestand hinaus zulassen.
public final class ConcurrencyStressHarness {
  private final AtomicInteger available;
  public ConcurrencyStressHarness(int initial) {
    if (initial < 0) throw new IllegalArgumentException();
    this.available = new AtomicInteger(initial);
  }

  public boolean reserveOne() {
    while (true) {
      int current = available.get();
      if (current == 0) return false;
      if (available.compareAndSet(current, current - 1)) return true;
    }
  }

  public int available() { return available.get(); }
}

JUnit-Test

ConcurrencyStressHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class ConcurrencyStressHarnessTest {
  @Test void never_reserves_more_than_available_stock() throws Exception {
    int stock = 25;
    var inventory = new ConcurrencyStressHarness(stock);
    var start = new CountDownLatch(1);
    var successes = new AtomicInteger();
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      List<Future<?>> tasks = new ArrayList<>();
      for (int i = 0; i < 200; i++) {
        tasks.add(executor.submit(() -> {
          start.await();
          if (inventory.reserveOne()) successes.incrementAndGet();
          return null;
        }));
      }
      start.countDown();
      for (var task : tasks) task.get();
    }
    assertAll(
      () -> assertEquals(stock, successes.get()),
      () -> assertEquals(0, inventory.available())
    );
  }
}
Refactoring-Regel: Nebenläufigkeit mit synchronisiertem Start und fachlicher Endinvariante prüfen, nicht mit zufälligen Sleeps.
Kapitel 4 Testcontainers und reproduzierbare Infrastrukturtests5 Praxislabore

Fünf Labs machen Integrationstests belastbar: Ressourcen-Lebenszyklus, Transaktionsisolation, HTTP- und Messaging-Verträge sowie ausführbare Architekturregeln.

Lab 16 Testcontainers Fixture Boundary Infrastruktur-Lebenszyklen in eine explizite Fixture kapseln und Tests nur gegen veröffentlichte Verbindungsdaten koppeln.

Testcontainers Fixture Boundary

Code Smell

Container-Start, Ports und Cleanup werden in jedem Test neu und unterschiedlich verdrahtet.

Pattern / Prinzip

Resource Fixture + Lifecycle Owner

Testfokus

Ein zentraler Fixture-Owner stellt stabile Verbindungsdaten bereit und garantiert deterministisches Cleanup.

Alternative

Framework-spezifische Container-Extension, wenn ausschließlich ein Testframework verwendet wird.

Vor dem Refactoring

schwacher Testvertrag
// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.

Referenzlösung

TestcontainersFixtureBoundary.java
package com.aydinsude.workbench.testing;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

// Testing Pattern: Resource Fixture + Lifecycle Owner
// Zweck: Lebenszyklus externer Testressourcen zentral und deterministisch verwalten.
public final class TestcontainersFixtureBoundary implements AutoCloseable {
  public interface RuntimeResource extends AutoCloseable {
    void start();
    String endpoint();
    @Override void close();
  }

  private final RuntimeResource resource;
  private final AtomicBoolean started = new AtomicBoolean();

  public TestcontainersFixtureBoundary(RuntimeResource resource) {
    this.resource = Objects.requireNonNull(resource);
  }

  public String startOnce() {
    if (started.compareAndSet(false, true)) resource.start();
    return resource.endpoint();
  }

  @Override public void close() {
    if (started.compareAndSet(true, false)) resource.close();
  }
}

JUnit-Test

TestcontainersFixtureBoundaryTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class TestcontainersFixtureBoundaryTest {
  @Test void starts_once_and_closes_once() {
    var starts = new AtomicInteger(); var closes = new AtomicInteger();
    var resource = new TestcontainersFixtureBoundary.RuntimeResource() {
      public void start() { starts.incrementAndGet(); }
      public String endpoint() { return "jdbc:test://localhost/db"; }
      public void close() { closes.incrementAndGet(); }
    };
    var fixture = new TestcontainersFixtureBoundary(resource);
    assertEquals("jdbc:test://localhost/db", fixture.startOnce());
    fixture.startOnce(); fixture.close(); fixture.close();
    assertAll(() -> assertEquals(1, starts.get()), () -> assertEquals(1, closes.get()));
  }
}
Refactoring-Regel: Infrastruktur-Lebenszyklen in eine explizite Fixture kapseln und Tests nur gegen veröffentlichte Verbindungsdaten koppeln.

Lab 17 Transaction Rollback Harness Testdaten über eine explizite Transaktionsgrenze isolieren; Cleanup darf kein optionaler Nachsatz sein.

Transaction Rollback Harness

Code Smell

Integrationstests hinterlassen Daten oder reinigen Tabellen manuell in fehleranfälliger Reihenfolge.

Pattern / Prinzip

Transaction Script + Test Fixture

Testfokus

Jeder Test läuft in einer kontrollierten Transaktion und beendet sich garantiert mit Rollback.

Alternative

Schema-per-Test, wenn parallele Tests vollständige Isolation benötigen.

Vor dem Refactoring

schwacher Testvertrag
// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.

Referenzlösung

TransactionRollbackHarness.java
package com.aydinsude.workbench.testing;

import java.util.Objects;
import java.util.function.Supplier;

// Testing Pattern: Transaction Script + Test Fixture
// Zweck: Integrationstests atomar ausfuehren und Testdaten garantiert zurueckrollen.
public final class TransactionRollbackHarness {
  public interface Transaction { void rollback(); }
  public interface TransactionManager { Transaction begin(); }
  private final TransactionManager manager;
  public TransactionRollbackHarness(TransactionManager manager) { this.manager = Objects.requireNonNull(manager); }

  public <T> T execute(Supplier<T> testBody) {
    Transaction tx = manager.begin();
    try { return testBody.get(); }
    finally { tx.rollback(); }
  }
}

JUnit-Test

TransactionRollbackHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;

class TransactionRollbackHarnessTest {
  @Test void rolls_back_after_success_and_failure() {
    var rolledBack = new AtomicBoolean();
    var harness = new TransactionRollbackHarness(() -> () -> rolledBack.set(true));
    assertEquals("ok", harness.execute(() -> "ok"));
    assertTrue(rolledBack.get());
    rolledBack.set(false);
    assertThrows(IllegalStateException.class, () -> harness.execute(() -> { throw new IllegalStateException("boom"); }));
    assertTrue(rolledBack.get());
  }
}
Refactoring-Regel: Testdaten über eine explizite Transaktionsgrenze isolieren; Cleanup darf kein optionaler Nachsatz sein.

Lab 18 HTTP Contract Stub HTTP-Verträge an der Protokollgrenze testen, nicht hinter einem bereits gemockten Client-Adapter.

HTTP Contract Stub

Code Smell

HTTP-Tests mocken interne Clientmethoden und übersehen Pfad, Header, Statuscode oder Payload-Vertrag.

Pattern / Prinzip

Stub Server + Contract Object

Testfokus

Der Stub zeichnet echte Request-Metadaten auf und antwortet über einen expliziten Vertrag.

Alternative

Consumer-Driven Contract Framework, wenn mehrere Teams denselben Provider unabhängig entwickeln.

Vor dem Refactoring

schwacher Testvertrag
// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.

Referenzlösung

HttpContractStub.java
package com.aydinsude.workbench.testing;

import java.util.Map;
import java.util.Objects;

// Testing Pattern: Stub Server + Contract Object
// Zweck: HTTP-Vertraege ueber Methode, Pfad, Header, Status und Body sichtbar testen.
public final class HttpContractStub {
  public record Request(String method, String path, Map<String,String> headers, String body) {
    public Request { headers = Map.copyOf(headers); }
  }
  public record Response(int status, String body) {}
  public interface Contract { Response handle(Request request); }
  private final Contract contract;
  private Request lastRequest;
  public HttpContractStub(Contract contract) { this.contract = Objects.requireNonNull(contract); }
  public Response exchange(Request request) { lastRequest = Objects.requireNonNull(request); return contract.handle(request); }
  public Request lastRequest() { return lastRequest; }
}

JUnit-Test

HttpContractStubTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;

class HttpContractStubTest {
  @Test void verifies_protocol_contract() {
    var stub = new HttpContractStub(req -> new HttpContractStub.Response(201, "created:" + req.body()));
    var response = stub.exchange(new HttpContractStub.Request("POST", "/orders", Map.of("Content-Type","application/json"), "{\"id\":7}"));
    assertAll(() -> assertEquals(201, response.status()),
      () -> assertEquals("POST", stub.lastRequest().method()),
      () -> assertEquals("/orders", stub.lastRequest().path()),
      () -> assertEquals("application/json", stub.lastRequest().headers().get("Content-Type")));
  }
}
Refactoring-Regel: HTTP-Verträge an der Protokollgrenze testen, nicht hinter einem bereits gemockten Client-Adapter.

Lab 19 Message Consumer Contract Message Consumer gegen den vollständigen Envelope-Vertrag testen, nicht nur gegen den deserialisierten Payload.

Message Consumer Contract

Code Smell

Messaging-Tests rufen Handler direkt mit beliebigen Strings auf und ignorieren Schlüssel, Version, Header und Duplikate.

Pattern / Prinzip

Consumer Contract + Envelope

Testfokus

Ein typisierter Envelope macht Versionierung, Korrelation und Idempotenz zum Testvertrag.

Alternative

Schema Registry Compatibility Tests für organisationsweite Event-Governance.

Vor dem Refactoring

schwacher Testvertrag
// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.

Referenzlösung

MessageConsumerContract.java
package com.aydinsude.workbench.testing;

import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

// Testing Pattern: Consumer Contract + Envelope + Idempotent Consumer
// Zweck: Version, Korrelation und Duplikatschutz als pruefbaren Nachrichtenvertrag modellieren.
public final class MessageConsumerContract {
  public record Envelope(String messageId, String type, int version, String payload, Map<String,String> headers) {
    public Envelope { headers = Map.copyOf(headers); }
  }
  public interface Handler { void handle(String payload); }
  private final Set<String> processed = ConcurrentHashMap.newKeySet();
  private final Handler handler;
  public MessageConsumerContract(Handler handler) { this.handler = Objects.requireNonNull(handler); }
  public boolean consume(Envelope envelope) {
    if (!"OrderCreated".equals(envelope.type()) || envelope.version() != 1) throw new IllegalArgumentException("unsupported contract");
    if (!processed.add(envelope.messageId())) return false;
    handler.handle(envelope.payload());
    return true;
  }
}

JUnit-Test

MessageConsumerContractTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;

class MessageConsumerContractTest {
  @Test void validates_envelope_and_rejects_duplicate_delivery() {
    var calls = new AtomicInteger();
    var consumer = new MessageConsumerContract(payload -> calls.incrementAndGet());
    var event = new MessageConsumerContract.Envelope("m-1", "OrderCreated", 1, "order-7", Map.of("correlationId","c-1"));
    assertTrue(consumer.consume(event));
    assertFalse(consumer.consume(event));
    assertEquals(1, calls.get());
    assertThrows(IllegalArgumentException.class, () -> consumer.consume(new MessageConsumerContract.Envelope("m-2","OrderCreated",2,"x",Map.of())));
  }
}
Refactoring-Regel: Message Consumer gegen den vollständigen Envelope-Vertrag testen, nicht nur gegen den deserialisierten Payload.

Lab 20 Architecture Fitness Rule Wichtige Architekturentscheidungen als ausführbare Fitness Functions dauerhaft im Build verankern.

Architecture Fitness Rule

Code Smell

Architekturregeln stehen nur in Diagrammen; verbotene Abhängigkeiten werden erst im Review entdeckt.

Pattern / Prinzip

Fitness Function + Dependency Rule

Testfokus

Eine ausführbare Regel prüft Paketabhängigkeiten und liefert präzise Verletzungen.

Alternative

ArchUnit, wenn Bytecode-basierte Regeln und umfangreiche DSL benötigt werden.

Vor dem Refactoring

schwacher Testvertrag
// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.

Referenzlösung

ArchitectureFitnessRule.java
package com.aydinsude.workbench.testing;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

// Architecture Pattern: Fitness Function + Dependency Rule
// Zweck: Verbotene Abhaengigkeiten automatisiert und dauerhaft pruefen.
public final class ArchitectureFitnessRule {
  public record Dependency(String sourcePackage, String targetPackage) {}
  private final Set<String> forbiddenTargets;
  public ArchitectureFitnessRule(Set<String> forbiddenTargets) { this.forbiddenTargets = Set.copyOf(forbiddenTargets); }
  public List<Dependency> violations(List<Dependency> dependencies) {
    return dependencies.stream()
      .filter(d -> d.sourcePackage().contains("domain"))
      .filter(d -> forbiddenTargets.stream().anyMatch(d.targetPackage()::startsWith))
      .collect(Collectors.toUnmodifiableList());
  }
}

JUnit-Test

ArchitectureFitnessRuleTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;

class ArchitectureFitnessRuleTest {
  @Test void domain_must_not_depend_on_framework_or_persistence_packages() {
    var rule = new ArchitectureFitnessRule(Set.of("org.springframework", "jakarta.persistence"));
    var dependencies = List.of(
      new ArchitectureFitnessRule.Dependency("com.acme.domain.order", "java.time"),
      new ArchitectureFitnessRule.Dependency("com.acme.domain.order", "jakarta.persistence.Entity"),
      new ArchitectureFitnessRule.Dependency("com.acme.adapter.web", "org.springframework.web"));
    var violations = rule.violations(dependencies);
    assertAll(() -> assertEquals(1, violations.size()),
      () -> assertEquals("jakarta.persistence.Entity", violations.getFirst().targetPackage()));
  }
}
Refactoring-Regel: Wichtige Architekturentscheidungen als ausführbare Fitness Functions dauerhaft im Build verankern.
Kapitel 5 Snapshots, Approval Tests und semantische Vergleiche5 Praxislabore

Fünf Labs stärken Testoracles, breite Eingaberäume und zeitlich überlappende Systemverträge - ohne Tests an interne Implementierungsdetails zu koppeln.

Lab 21 Semantic Snapshot Approval Snapshots erst nach fachlicher Normalisierung freigeben; niemals volatile Rohdaten als Vertrag behandeln.

Semantic Snapshot Approval

Code Smell

Snapshots enthalten Zeitstempel, zufällige IDs oder instabile Reihenfolgen und erzeugen dadurch Rauschen statt Vertrauen.

Pattern / Prinzip

Approval Testing + Canonicalizer

Testfokus

Vor dem Vergleich werden volatile Felder entfernt und fachlich relevante Reihenfolgen kanonisiert.

Alternative

Explizite Feldassertions, wenn das Ergebnis klein und die relevanten Felder vollständig bekannt sind.

Vor dem Refactoring

schwacher Testvertrag
// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.

Referenzlösung

SemanticSnapshotApproval.java
package com.aydinsude.workbench.testing;

import java.util.Objects;
import java.util.function.UnaryOperator;

// Testing Pattern: Approval Testing + Canonicalizer
// Zweck: stabile fachliche Snapshots von volatilen technischen Details trennen.
public final class SemanticSnapshotApproval {
  private final UnaryOperator<String> canonicalizer;

  public SemanticSnapshotApproval(UnaryOperator<String> canonicalizer) {
    this.canonicalizer = Objects.requireNonNull(canonicalizer);
  }

  public ApprovalResult compare(String expected, String actual) {
    String normalizedExpected = canonicalizer.apply(expected);
    String normalizedActual = canonicalizer.apply(actual);
    return new ApprovalResult(normalizedExpected.equals(normalizedActual),
        normalizedExpected, normalizedActual);
  }

  public record ApprovalResult(boolean approved, String expected, String actual) {}
}

JUnit-Test

SemanticSnapshotApprovalTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class SemanticSnapshotApprovalTest {
  @Test void ignores_volatile_request_id() {
    var approval = new SemanticSnapshotApproval(value ->
        value.replaceAll("request-[0-9]+", "request-<id>"));
    var result = approval.compare("status=OK;id=request-1", "status=OK;id=request-99");
    assertTrue(result.approved());
  }
}
Refactoring-Regel: Snapshots erst nach fachlicher Normalisierung freigeben; niemals volatile Rohdaten als Vertrag behandeln.

Lab 22 Property-Based Case Generator Zufall nur hinter einem expliziten Seed verwenden und bei jedem Fehler das reproduzierbare Gegenbeispiel ausgeben.

Property-Based Case Generator

Code Smell

Beispieltests prüfen nur wenige handverlesene Werte und übersehen Grenzfälle oder unerwartete Kombinationen.

Pattern / Prinzip

Generator Boundary + Deterministic Seed

Testfokus

Ein reproduzierbarer Generator liefert viele Fälle; bei Fehlern bleibt der Seed als Gegenbeispiel erhalten.

Alternative

Decision Tables, wenn der Eingaberaum klein, endlich und fachlich vollständig aufzählbar ist.

Vor dem Refactoring

schwacher Testvertrag
// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.

Referenzlösung

PropertyCaseGenerator.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.IntPredicate;

// Testing Pattern: Generator Boundary + Deterministic Seed
// Zweck: breite Eingaberäume reproduzierbar prüfen.
public final class PropertyCaseGenerator {
  public record Case(int value, long seed, int index) {}

  public List<Case> integers(long seed, int count, int minimum, int maximum) {
    if (count < 0 || minimum > maximum) throw new IllegalArgumentException("invalid range");
    Random random = new Random(seed);
    List<Case> cases = new ArrayList<>(count);
    int width = maximum - minimum + 1;
    for (int i = 0; i < count; i++) {
      cases.add(new Case(minimum + random.nextInt(width), seed, i));
    }
    return List.copyOf(cases);
  }

  public Case firstViolation(List<Case> cases, IntPredicate property) {
    return cases.stream().filter(value -> !property.test(value.value())).findFirst().orElse(null);
  }
}

JUnit-Test

PropertyCaseGeneratorTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class PropertyCaseGeneratorTest {
  @Test void same_seed_produces_same_cases() {
    var generator = new PropertyCaseGenerator();
    assertEquals(generator.integers(42L, 100, -10, 10),
        generator.integers(42L, 100, -10, 10));
    assertNull(generator.firstViolation(generator.integers(42L, 100, 0, 10), value -> value >= 0));
  }
}
Refactoring-Regel: Zufall nur hinter einem expliziten Seed verwenden und bei jedem Fehler das reproduzierbare Gegenbeispiel ausgeben.

Lab 23 Interaction Spy Boundary Interaktionen nur an stabilen Systemgrenzen prüfen; interne Aufrufreihenfolgen sind kein fachlicher Vertrag.

Interaction Spy Boundary

Code Smell

Tests mocken interne Methodenketten und brechen bei jeder harmlosen Umstrukturierung, obwohl das fachliche Verhalten gleich bleibt.

Pattern / Prinzip

Recording Spy + Observable Port

Testfokus

Nur fachlich relevante Interaktionen an einem öffentlichen Port werden aufgezeichnet und gezielt ausgewertet.

Alternative

State Verification, wenn der resultierende Zustand den Vertrag vollständig beschreibt.

Vor dem Refactoring

schwacher Testvertrag
// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.

Referenzlösung

InteractionSpyBoundary.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

// Testing Pattern: Recording Spy + Observable Port
// Zweck: fachlich relevante Ausgaben prüfen, ohne interne Implementierung zu koppeln.
public final class InteractionSpyBoundary {
  public interface NotificationPort { void send(String recipient, String template); }
  public record Interaction(String recipient, String template) {}
  private final List<Interaction> interactions = new ArrayList<>();

  public void send(String recipient, String template) {
    interactions.add(new Interaction(Objects.requireNonNull(recipient), Objects.requireNonNull(template)));
  }

  public List<Interaction> interactions() { return List.copyOf(interactions); }
  public long countTemplate(String template) {
    return interactions.stream().filter(it -> it.template().equals(template)).count();
  }
}

JUnit-Test

InteractionSpyBoundaryTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class InteractionSpyBoundaryTest {
  @Test void records_only_public_boundary_interactions() {
    var spy = new InteractionSpyBoundary();
    spy.send("customer@example.test", "ORDER_CONFIRMED");
    assertEquals(1, spy.countTemplate("ORDER_CONFIRMED"));
    assertEquals("customer@example.test", spy.interactions().getFirst().recipient());
  }
}
Refactoring-Regel: Interaktionen nur an stabilen Systemgrenzen prüfen; interne Aufrufreihenfolgen sind kein fachlicher Vertrag.

Lab 24 Database Migration Compatibility Datenbankmigrationen als zeitlich überlappenden Vertrag prüfen und Expand vor Contract erzwingen.

Database Migration Compatibility

Code Smell

Migrationen werden nur auf syntaktische Ausführbarkeit geprüft; alte Anwendungsversionen können danach trotzdem nicht mehr lesen oder schreiben.

Pattern / Prinzip

Compatibility Test + Expand/Contract

Testfokus

Ein maschinenlesbares Schema-Modell prüft, dass bestehende Spalten erhalten und neue Pflichtfelder kompatibel eingeführt werden.

Alternative

Containerbasierter Vorwärts-/Rückwärts-Test gegen eine echte Datenbank für provider-spezifische Semantik.

Vor dem Refactoring

schwacher Testvertrag
// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.

Referenzlösung

SchemaCompatibilityVerifier.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

// Testing Pattern: Compatibility Test + Expand/Contract
// Zweck: überlappende Anwendungsversionen bei Datenbankmigrationen schützen.
public final class SchemaCompatibilityVerifier {
  public record Column(String type, boolean nullable, boolean hasDefault) {}
  public record Violation(String column, String reason) {}

  public List<Violation> verify(Map<String, Column> before, Map<String, Column> after) {
    List<Violation> violations = new ArrayList<>();
    before.forEach((name, oldColumn) -> {
      Column newColumn = after.get(name);
      if (newColumn == null) violations.add(new Violation(name, "removed during overlap"));
      else if (!oldColumn.type().equals(newColumn.type()))
        violations.add(new Violation(name, "type changed"));
    });
    after.forEach((name, column) -> {
      if (!before.containsKey(name) && !column.nullable() && !column.hasDefault())
        violations.add(new Violation(name, "new required column has no default"));
    });
    return List.copyOf(violations);
  }
}

JUnit-Test

SchemaCompatibilityVerifierTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;

class SchemaCompatibilityVerifierTest {
  @Test void rejects_required_column_without_default() {
    var verifier = new SchemaCompatibilityVerifier();
    var before = Map.of("id", new SchemaCompatibilityVerifier.Column("uuid", false, false));
    var after = Map.of(
        "id", new SchemaCompatibilityVerifier.Column("uuid", false, false),
        "tenant", new SchemaCompatibilityVerifier.Column("text", false, false));
    assertEquals("tenant", verifier.verify(before, after).getFirst().column());
  }
}
Refactoring-Regel: Datenbankmigrationen als zeitlich überlappenden Vertrag prüfen und Expand vor Contract erzwingen.

Lab 25 Consumer-Driven API Contract Nur tatsächlich benötigte Consumer-Erwartungen vertraglich festhalten und additive Provider-Änderungen erlauben.

Consumer-Driven API Contract

Code Smell

Provider-Tests prüfen nur den eigenen Controller, aber nicht die Felder und Bedeutungen, auf die reale Consumer angewiesen sind.

Pattern / Prinzip

Consumer Contract + Contract Fixture

Testfokus

Der Consumer veröffentlicht einen kleinen stabilen Vertrag; Provider-Antworten werden dagegen unabhängig von internen DTOs geprüft.

Alternative

Schema-First OpenAPI-Validierung, wenn alle Consumer denselben zentral verwalteten Vertrag akzeptieren.

Vor dem Refactoring

schwacher Testvertrag
// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.

Referenzlösung

ConsumerContractFixture.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

// Testing Pattern: Consumer Contract + Contract Fixture
// Zweck: reale Consumer-Erwartungen unabhängig von Provider-Interna prüfen.
public final class ConsumerContractFixture {
  public record Contract(Set<String> requiredFields, Map<String, Class<?>> fieldTypes) {}
  public record Violation(String field, String reason) {}

  public List<Violation> verify(Contract contract, Map<String, Object> response) {
    List<Violation> violations = new ArrayList<>();
    for (String field : contract.requiredFields()) {
      if (!response.containsKey(field)) violations.add(new Violation(field, "missing"));
    }
    contract.fieldTypes().forEach((field, type) -> {
      Object value = response.get(field);
      if (value != null && !type.isInstance(value)) violations.add(new Violation(field, "wrong type"));
    });
    return List.copyOf(violations);
  }
}

JUnit-Test

ConsumerContractFixtureTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;

class ConsumerContractFixtureTest {
  @Test void allows_additive_fields_but_rejects_missing_required_field() {
    var fixture = new ConsumerContractFixture();
    var contract = new ConsumerContractFixture.Contract(Set.of("id", "status"),
        Map.of("id", String.class, "status", String.class));
    assertTrue(fixture.verify(contract, Map.of("id", "42", "status", "OPEN", "extra", 7)).isEmpty());
    assertEquals("status", fixture.verify(contract, Map.of("id", "42")).getFirst().field());
  }
}
Refactoring-Regel: Nur tatsächlich benötigte Consumer-Erwartungen vertraglich festhalten und additive Provider-Änderungen erlauben.
Kapitel 6 Grenzwerte, Partitionen und datengetriebene Tests5 Praxislabore

Fünf Labs machen Grenzwerte, fehlende Oracles, Fehlerpfade, Ressourcenbesitz und Performance-Budgets als stabile Testverträge ausführbar.

Lab 26 Boundary Value Matrix Fachliche Grenzen als benannte Partitionen modellieren und jeden Übergang explizit prüfen.

Boundary Value Matrix

Code Smell

Grenzwerte werden nur zufällig oder durch wenige Happy-Path-Beispiele geprüft; Off-by-one-Fehler bleiben unentdeckt.

Pattern / Prinzip

Boundary Matrix + Equivalence Partitions

Testfokus

Die Testmatrix erzeugt Werte direkt unter, auf und über jeder relevanten Fachgrenze und ordnet sie erwarteten Partitionen zu.

Alternative

Property-Based Testing, wenn viele kombinierte Grenzen und große Eingaberäume untersucht werden müssen.

Vor dem Refactoring

schwacher Testvertrag
// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.

Referenzlösung

BoundaryValueMatrix.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.function.IntFunction;

// Testing Pattern: Boundary Matrix + Equivalence Partitions
// Zweck: Fachgrenzen systematisch mit Werten unterhalb, auf und oberhalb absichern.
public final class BoundaryValueMatrix {
  public record Case(int input, String expectedPartition) {}
  public record Result(Case testCase, String actualPartition, boolean matches) {}

  public List<Case> around(int boundary, String lower, String upper) {
    return List.of(
        new Case(boundary - 1, lower),
        new Case(boundary, upper),
        new Case(boundary + 1, upper));
  }

  public List<Result> verify(List<Case> cases, IntFunction<String> classifier) {
    List<Result> results = new ArrayList<>();
    for (Case testCase : cases) {
      String actual = classifier.apply(testCase.input());
      results.add(new Result(testCase, actual, testCase.expectedPartition().equals(actual)));
    }
    return List.copyOf(results);
  }
}

JUnit-Test

BoundaryValueMatrixTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class BoundaryValueMatrixTest {
  @Test void verifies_values_around_free_shipping_threshold() {
    var matrix = new BoundaryValueMatrix();
    var results = matrix.verify(matrix.around(100, "PAID", "FREE"),
        amount -> amount >= 100 ? "FREE" : "PAID");
    assertTrue(results.stream().allMatch(BoundaryValueMatrix.Result::matches));
  }
}
Refactoring-Regel: Fachliche Grenzen als benannte Partitionen modellieren und jeden Übergang explizit prüfen.

Lab 27 Metamorphic Test Oracle Wenn das exakte Oracle fehlt, fachlich zwingende Beziehungen zwischen mehreren Ausführungen prüfen.

Metamorphic Test Oracle

Code Smell

Für komplexe Berechnungen ist kein vollständiges erwartetes Ergebnis bekannt; Tests vergleichen deshalb nur einzelne Beispiele.

Pattern / Prinzip

Metamorphic Testing + Relation Oracle

Testfokus

Statt exakter Ergebnisse werden stabile Beziehungen geprüft, etwa Monotonie, Skalierung oder Permutationsinvarianz.

Alternative

Golden Master, wenn ein vertrauenswürdiger Legacy-Ausgangszustand bereits vorhanden ist.

Vor dem Refactoring

schwacher Testvertrag
// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.

Referenzlösung

MetamorphicTestOracle.java
package com.aydinsude.workbench.testing;

import java.math.BigDecimal;
import java.util.Objects;
import java.util.function.UnaryOperator;

// Testing Pattern: Metamorphic Testing + Relation Oracle
// Zweck: Systeme ohne einfaches exaktes Oracle über fachliche Beziehungen prüfen.
public final class MetamorphicTestOracle {
  public record Observation(BigDecimal original, BigDecimal transformed, boolean relationHolds) {}

  public Observation verifyScale(BigDecimal input, BigDecimal factor,
      UnaryOperator<BigDecimal> calculation) {
    Objects.requireNonNull(input);
    Objects.requireNonNull(factor);
    BigDecimal original = calculation.apply(input);
    BigDecimal transformed = calculation.apply(input.multiply(factor));
    boolean holds = transformed.compareTo(original.multiply(factor)) == 0;
    return new Observation(original, transformed, holds);
  }
}

JUnit-Test

MetamorphicTestOracleTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class MetamorphicTestOracleTest {
  @Test void proportional_fee_scales_with_input() {
    var oracle = new MetamorphicTestOracle();
    var result = oracle.verifyScale(new BigDecimal("100"), new BigDecimal("3"),
        amount -> amount.multiply(new BigDecimal("0.02")));
    assertTrue(result.relationHolds());
  }
}
Refactoring-Regel: Wenn das exakte Oracle fehlt, fachlich zwingende Beziehungen zwischen mehreren Ausführungen prüfen.

Lab 28 Fault Injection Harness Fehler kontrolliert an Ports injizieren und Recovery-Verhalten getrennt von zufälligen Infrastrukturstörungen testen.

Fault Injection Harness

Code Smell

Fehlerpfade werden nur durch schwer reproduzierbare Infrastrukturstörungen erreicht und bleiben dadurch praktisch ungetestet.

Pattern / Prinzip

Fault Injection + Controlled Failure Port

Testfokus

Ein expliziter Fehlerplan injiziert definierte Ausfälle an stabilen Ports und dokumentiert, welcher Aufruf scheitern soll.

Alternative

Chaos Engineering in einer produktionsnahen Umgebung, wenn Systemverhalten über mehrere echte Dienste geprüft werden muss.

Vor dem Refactoring

schwacher Testvertrag
// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.

Referenzlösung

FaultInjectionHarness.java
package com.aydinsude.workbench.testing;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

// Testing Pattern: Fault Injection + Controlled Failure Port
// Zweck: Fehlerpfade deterministisch und reproduzierbar auslösen.
public final class FaultInjectionHarness {
  private final int failOnCall;
  private final AtomicInteger calls = new AtomicInteger();

  public FaultInjectionHarness(int failOnCall) {
    if (failOnCall < 1) throw new IllegalArgumentException("failOnCall must be positive");
    this.failOnCall = failOnCall;
  }

  public <T> T execute(Supplier<T> operation) {
    int current = calls.incrementAndGet();
    if (current == failOnCall) throw new InjectedFailure("injected failure on call " + current);
    return operation.get();
  }

  public int calls() { return calls.get(); }
  public static final class InjectedFailure extends RuntimeException {
    public InjectedFailure(String message) { super(message); }
  }
}

JUnit-Test

FaultInjectionHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class FaultInjectionHarnessTest {
  @Test void fails_exactly_on_configured_call() {
    var harness = new FaultInjectionHarness(2);
    assertEquals("ok", harness.execute(() -> "ok"));
    assertThrows(FaultInjectionHarness.InjectedFailure.class,
        () -> harness.execute(() -> "never"));
    assertEquals(2, harness.calls());
  }
}
Refactoring-Regel: Fehler kontrolliert an Ports injizieren und Recovery-Verhalten getrennt von zufälligen Infrastrukturstörungen testen.

Lab 29 Resource Leak Probe Ressourcenbesitz explizit machen und nach Erfolg wie Fehler die Invariante geöffnet gleich geschlossen prüfen.

Resource Leak Probe

Code Smell

Tests prüfen nur das fachliche Ergebnis, aber nicht, ob Streams, Sessions oder Handles nach Fehlern zuverlässig geschlossen werden.

Pattern / Prinzip

Lifecycle Probe + Resource Accounting

Testfokus

Ein zählender Lifecycle-Port macht Öffnen und Schließen sichtbar und prüft nach jeder Ausführung die Ressourceninvariante.

Alternative

Profiler oder OS-Level-Metriken für native Ressourcen und langfristige Lasttests.

Vor dem Refactoring

schwacher Testvertrag
// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.

Referenzlösung

ResourceLeakProbe.java
package com.aydinsude.workbench.testing;

import java.util.concurrent.atomic.AtomicInteger;

// Testing Pattern: Lifecycle Probe + Resource Accounting
// Zweck: Ressourcenleaks als ausführbare Invariante sichtbar machen.
public final class ResourceLeakProbe {
  private final AtomicInteger opened = new AtomicInteger();
  private final AtomicInteger closed = new AtomicInteger();

  public Lease open() {
    opened.incrementAndGet();
    return new Lease();
  }

  public boolean balanced() { return opened.get() == closed.get(); }
  public int openCount() { return opened.get() - closed.get(); }

  public final class Lease implements AutoCloseable {
    private boolean isClosed;
    @Override public void close() {
      if (!isClosed) {
        isClosed = true;
        closed.incrementAndGet();
      }
    }
  }
}

JUnit-Test

ResourceLeakProbeTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class ResourceLeakProbeTest {
  @Test void try_with_resources_balances_lifecycle() {
    var probe = new ResourceLeakProbe();
    try (var ignored = probe.open()) {
      assertEquals(1, probe.openCount());
    }
    assertTrue(probe.balanced());
  }
}
Refactoring-Regel: Ressourcenbesitz explizit machen und nach Erfolg wie Fehler die Invariante geöffnet gleich geschlossen prüfen.

Lab 30 Performance Budget Test Performance nur mit explizitem Budget und passender Messebene prüfen; Nano-Benchmarks nicht als End-to-End-Beweis missbrauchen.

Performance Budget Test

Code Smell

Performance wird nur subjektiv bewertet oder erst nach Produktionsproblemen gemessen; Regressionen haben keinen ausführbaren Grenzwert.

Pattern / Prinzip

Performance Budget + Measurement Boundary

Testfokus

Ein Messport trennt Zeitquelle, Warmup und Budgetentscheidung; Tests prüfen klar definierte Service-Level-Grenzen.

Alternative

JMH für präzise Mikrobenchmarks oder Lasttests für End-to-End-Durchsatz und Perzentile.

Vor dem Refactoring

schwacher Testvertrag
// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.

Referenzlösung

PerformanceBudget.java
package com.aydinsude.workbench.testing;

import java.time.Duration;
import java.util.Objects;
import java.util.function.LongSupplier;

// Testing Pattern: Performance Budget + Measurement Boundary
// Zweck: Performance-Regressionsgrenzen als ausführbaren Vertrag formulieren.
public final class PerformanceBudget {
  private final LongSupplier nanoTime;

  public PerformanceBudget(LongSupplier nanoTime) {
    this.nanoTime = Objects.requireNonNull(nanoTime);
  }

  public Measurement measure(Runnable operation, Duration budget) {
    long start = nanoTime.getAsLong();
    operation.run();
    long elapsed = nanoTime.getAsLong() - start;
    return new Measurement(Duration.ofNanos(elapsed), budget,
        elapsed <= budget.toNanos());
  }

  public record Measurement(Duration elapsed, Duration budget, boolean withinBudget) {}
}

JUnit-Test

PerformanceBudgetTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.Test;

class PerformanceBudgetTest {
  @Test void deterministic_clock_detects_budget_violation() {
    var clock = new AtomicLong();
    var budget = new PerformanceBudget(() -> clock.getAndAdd(8_000_000));
    var result = budget.measure(() -> {}, Duration.ofMillis(5));
    assertFalse(result.withinBudget());
    assertEquals(Duration.ofMillis(8), result.elapsed());
  }
}
Refactoring-Regel: Performance nur mit explizitem Budget und passender Messebene prüfen; Nano-Benchmarks nicht als End-to-End-Beweis missbrauchen.
Kapitel 7 Humble Object und testbare Framework-Grenzen5 Praxislabore

Fünf Labs entkoppeln Frameworks, asynchrone Beobachtung, strukturierte Logs, Mutationsergebnisse und Flake-Evidenz von instabilen Testdetails.

Lab 31 Humble Object Boundary Framework- und UI-Code auf Übersetzung reduzieren; fachliche Entscheidung in ein reines Objekt verschieben.

Humble Object Boundary

Code Smell

Fachlogik steckt direkt in Controller-, Listener- oder Framework-Callbacks und ist nur mit schwergewichtiger Laufzeit testbar.

Pattern / Prinzip

Humble Object + Functional Core

Testfokus

Die extrahierte Entscheidungslogik wird ohne Container, Netzwerk oder UI deterministisch getestet.

Alternative

Slice- oder Integrationstest, wenn die Framework-Bindung selbst das primäre Risiko ist.

Vor dem Refactoring

schwacher Testvertrag
// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

HumbleObjectBoundary.java
package com.aydinsude.workbench.testing;

import java.util.Objects;

// Testing Pattern: Humble Object + Functional Core
// Zweck: Framework-nahe Übersetzung von reiner, schnell testbarer Fachlogik trennen.
public final class HumbleObjectBoundary {
  public record Request(String customerId, int openInvoices, boolean blocked) {}
  public record Decision(boolean accepted, String reason) {}

  public Decision decide(Request request) {
    Objects.requireNonNull(request);
    if (request.blocked()) return new Decision(false, "CUSTOMER_BLOCKED");
    if (request.openInvoices() > 3) return new Decision(false, "TOO_MANY_OPEN_INVOICES");
    return new Decision(true, "ACCEPTED");
  }
}

JUnit-Test

HumbleObjectBoundaryTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class HumbleObjectBoundaryTest {
  @Test void rejects_blocked_customer_without_framework_runtime() {
    var decision = new HumbleObjectBoundary().decide(
        new HumbleObjectBoundary.Request("C-17", 0, true));
    assertFalse(decision.accepted());
    assertEquals("CUSTOMER_BLOCKED", decision.reason());
  }
}
Refactoring-Regel: Framework- und UI-Code auf Übersetzung reduzieren; fachliche Entscheidung in ein reines Objekt verschieben.

Lab 32 Async Event Probe Asynchrone Ergebnisse über einen fachlichen Probe-Port beobachten statt interne Threads oder Sleeps zu testen.

Async Event Probe

Code Smell

Asynchrone Tests greifen auf interne Futures zu oder warten mit festen Sleeps; Reihenfolge und Korrelation bleiben unklar.

Pattern / Prinzip

Test Probe + Event Recorder

Testfokus

Die Probe zeichnet korrelierte Ereignisse auf und erlaubt eine zeitbudgetierte, semantische Abfrage.

Alternative

Eventually Probe aus Abschnitt 2, wenn nur ein einzelner Zustand statt einer Ereignisfolge relevant ist.

Vor dem Refactoring

schwacher Testvertrag
// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

AsyncEventProbe.java
package com.aydinsude.workbench.testing;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

// Testing Pattern: Test Probe + Event Recorder
// Zweck: Asynchrone, korrelierte Ereignisse ohne Zugriff auf interne Threads beobachten.
public final class AsyncEventProbe {
  public record Event(String correlationId, String type, String payload) {}
  private final List<Event> events = new ArrayList<>();

  public synchronized void record(Event event) {
    events.add(Objects.requireNonNull(event));
    notifyAll();
  }

  public synchronized List<Event> await(String correlationId, int count, Duration timeout)
      throws InterruptedException {
    long deadline = System.nanoTime() + timeout.toNanos();
    while (matching(correlationId).size() < count) {
      long remaining = deadline - System.nanoTime();
      if (remaining <= 0) break;
      wait(Math.max(1L, remaining / 1_000_000L));
    }
    return matching(correlationId);
  }

  private List<Event> matching(String correlationId) {
    return events.stream().filter(e -> e.correlationId().equals(correlationId)).toList();
  }
}

JUnit-Test

AsyncEventProbeTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import org.junit.jupiter.api.Test;

class AsyncEventProbeTest {
  @Test void records_correlated_event_sequence() throws Exception {
    var probe = new AsyncEventProbe();
    probe.record(new AsyncEventProbe.Event("O-1", "CREATED", ""));
    probe.record(new AsyncEventProbe.Event("O-1", "APPROVED", ""));
    assertEquals(2, probe.await("O-1", 2, Duration.ofMillis(20)).size());
  }
}
Refactoring-Regel: Asynchrone Ergebnisse über einen fachlichen Probe-Port beobachten statt interne Threads oder Sleeps zu testen.

Lab 33 Structured Log Test Sink Logs als strukturierte Ereignisse hinter einem Port testen; niemals Textformat, globale Logger oder Konsole koppeln.

Structured Log Test Sink

Code Smell

Tests durchsuchen Konsolentext oder globale Logger-Ausgaben und brechen bei Layout-, Timestamp- oder Thread-Namen-Änderungen.

Pattern / Prinzip

Output Port + Recording Fake

Testfokus

Fachlich relevante Felder wie Eventtyp, Korrelation und Status werden strukturiert aufgezeichnet und gezielt geprüft.

Alternative

Tracing-Test-Exporter, wenn verteilte Spans und Parent-Child-Beziehungen der eigentliche Vertrag sind.

Vor dem Refactoring

schwacher Testvertrag
// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

StructuredLogTestSink.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

// Testing Pattern: Output Port + Recording Fake
// Zweck: Semantische Log-Ereignisse prüfen, ohne an Textformat oder globale Logger zu koppeln.
public final class StructuredLogTestSink {
  public record Entry(String event, String correlationId, Map<String, String> fields) {}
  private final List<Entry> entries = new ArrayList<>();

  public void accept(Entry entry) { entries.add(entry); }

  public List<Entry> byEvent(String event) {
    return entries.stream().filter(e -> e.event().equals(event)).toList();
  }

  public boolean contains(String event, String correlationId) {
    return entries.stream().anyMatch(e -> e.event().equals(event)
        && e.correlationId().equals(correlationId));
  }
}

JUnit-Test

StructuredLogTestSinkTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;

class StructuredLogTestSinkTest {
  @Test void verifies_semantic_fields_instead_of_rendered_text() {
    var sink = new StructuredLogTestSink();
    sink.accept(new StructuredLogTestSink.Entry("PAYMENT_REJECTED", "P-9",
        Map.of("reason", "LIMIT")));
    assertTrue(sink.contains("PAYMENT_REJECTED", "P-9"));
    assertEquals("LIMIT", sink.byEvent("PAYMENT_REJECTED").getFirst().fields().get("reason"));
  }
}
Refactoring-Regel: Logs als strukturierte Ereignisse hinter einem Port testen; niemals Textformat, globale Logger oder Konsole koppeln.

Lab 34 Mutation Score Gate Mutationsergebnisse als nachvollziehbares Qualitätsbudget behandeln; kritische überlebende Mutanten explizit ausweisen.

Mutation Score Gate

Code Smell

Hohe Coverage suggeriert Sicherheit, obwohl Assertions keine fachlichen Fehlentscheidungen erkennen.

Pattern / Prinzip

Quality Gate + Mutation Testing

Testfokus

Der Gate bewertet getötete, überlebende und ausgeschlossene Mutanten und liefert eine klare Freigabeentscheidung.

Alternative

Mutation-Resistant Assertions aus Abschnitt 3, wenn zunächst einzelne schwache Tests gezielt verbessert werden.

Vor dem Refactoring

schwacher Testvertrag
// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

MutationScoreGate.java
package com.aydinsude.workbench.testing;

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

// Testing Pattern: Quality Gate + Mutation Testing
// Zweck: Aussagekraft der Tests mit einem expliziten Mutationsbudget absichern.
public final class MutationScoreGate {
  public record Report(int killed, int survived, int excluded) {
    public Report {
      if (killed < 0 || survived < 0 || excluded < 0) throw new IllegalArgumentException();
    }
    public BigDecimal score() {
      int relevant = killed + survived;
      return relevant == 0 ? BigDecimal.ONE
          : BigDecimal.valueOf(killed).divide(BigDecimal.valueOf(relevant), 4, RoundingMode.HALF_UP);
    }
  }

  public record Decision(boolean passed, BigDecimal score, int survivors) {}

  public Decision evaluate(Report report, BigDecimal minimumScore, int maximumSurvivors) {
    BigDecimal score = report.score();
    return new Decision(score.compareTo(minimumScore) >= 0
        && report.survived() <= maximumSurvivors, score, report.survived());
  }
}

JUnit-Test

MutationScoreGateTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class MutationScoreGateTest {
  @Test void rejects_good_percentage_when_survivor_budget_is_exceeded() {
    var gate = new MutationScoreGate();
    var result = gate.evaluate(new MutationScoreGate.Report(98, 2, 10),
        new BigDecimal("0.95"), 1);
    assertFalse(result.passed());
    assertEquals(2, result.survivors());
  }
}
Refactoring-Regel: Mutationsergebnisse als nachvollziehbares Qualitätsbudget behandeln; kritische überlebende Mutanten explizit ausweisen.

Lab 35 Flake Repetition Harness Flaky-Verhalten mit kontrollierter Wiederholung, Seed und erster Fehlerursache sichtbar machen; nicht blind erneut ausführen.

Flake Repetition Harness

Code Smell

Instabile Tests werden pauschal wiederholt oder quarantänisiert, ohne Häufigkeit, Seed oder erste Abweichung zu dokumentieren.

Pattern / Prinzip

Repeatable Test Harness + Failure Evidence

Testfokus

Der Harness führt eine Operation begrenzt oft aus, stoppt beim ersten Fehler und liefert reproduzierbare Evidenz.

Alternative

Concurrency Stress Harness aus Abschnitt 3, wenn die Invariante unter echter paralleler Last geprüft werden muss.

Vor dem Refactoring

schwacher Testvertrag
// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

FlakeRepetitionHarness.java
package com.aydinsude.workbench.testing;

import java.util.Objects;

// Testing Pattern: Repeatable Test Harness + Failure Evidence
// Zweck: Flaky-Verhalten mit Iteration und erster Ursache reproduzierbar dokumentieren.
public final class FlakeRepetitionHarness {
  @FunctionalInterface public interface CheckedRun { void execute(int iteration) throws Exception; }
  public record Result(int planned, int completed, int failedAt, String error) {
    public boolean stable() { return failedAt < 0; }
  }

  public Result verify(int repetitions, CheckedRun run) {
    if (repetitions < 1) throw new IllegalArgumentException("repetitions");
    Objects.requireNonNull(run);
    for (int i = 1; i <= repetitions; i++) {
      try {
        run.execute(i);
      } catch (Exception failure) {
        return new Result(repetitions, i - 1, i, failure.getClass().getSimpleName()
            + ": " + String.valueOf(failure.getMessage()));
      }
    }
    return new Result(repetitions, repetitions, -1, "");
  }
}

JUnit-Test

FlakeRepetitionHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class FlakeRepetitionHarnessTest {
  @Test void reports_first_failing_iteration() {
    var result = new FlakeRepetitionHarness().verify(10, iteration -> {
      if (iteration == 4) throw new IllegalStateException("race observed");
    });
    assertFalse(result.stable());
    assertEquals(4, result.failedAt());
    assertTrue(result.error().contains("race observed"));
  }
}
Refactoring-Regel: Flaky-Verhalten mit kontrollierter Wiederholung, Seed und erster Fehlerursache sichtbar machen; nicht blind erneut ausführen.
Kapitel 8 Idempotenz, Replay und Seiteneffekt-Nachweise5 Praxislabore

Fünf Labs machen Idempotenz, Serialisierung, Retry-Verhalten, Parallelmigration und zeitliche Kopplung als reproduzierbare Testverträge sichtbar.

Lab 36 Idempotency Replay Harness Denselben fachlichen Befehl mehrfach abspielen und beweisen, dass genau ein Ergebnis und genau ein Seiteneffekt entsteht.

Idempotency Replay Harness

Code Smell

Wiederholte Zustellung wird nur im Happy Path geprüft; doppelte Befehle können denselben Seiteneffekt mehrfach auslösen.

Pattern / Prinzip

Replay Harness + Idempotency Key

Testfokus

Denselben fachlichen Befehl mehrfach abspielen und beweisen, dass genau ein Ergebnis und genau ein Seiteneffekt entsteht.

Alternative

Message Consumer Contract, wenn primär Envelope, Version und Korrelation statt Seiteneffekt-Deduplizierung geprüft werden.

Vor dem Refactoring

schwacher Testvertrag
// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.

Referenzlösung

IdempotencyReplayHarness.java
package com.aydinsude.workbench.testing;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

// Testing Pattern: Replay Harness + Idempotency Key
// Zweck: Wiederholte Ausführung mit demselben Schlüssel muss genau einen Seiteneffekt erzeugen.
public final class IdempotencyReplayHarness<T, R> {
  private final Map<String, R> results = new HashMap<>();
  private int executions;

  public R replay(String key, T command, int deliveries, Function<T, R> operation) {
    Objects.requireNonNull(key);
    if (deliveries < 1) throw new IllegalArgumentException("deliveries must be positive");
    R result = null;
    for (int i = 0; i < deliveries; i++) {
      result = results.computeIfAbsent(key, ignored -> {
        executions++;
        return operation.apply(command);
      });
    }
    return result;
  }

  public int executions() { return executions; }
}

JUnit-Test

IdempotencyReplayHarnessTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class IdempotencyReplayHarnessTest {
  @Test void repeated_delivery_executes_business_effect_once() {
    var harness = new IdempotencyReplayHarness<String, String>();
    var result = harness.replay("PAY-42", "charge", 5, command -> command.toUpperCase());
    assertEquals("CHARGE", result);
    assertEquals(1, harness.executions());
  }
}
Refactoring-Regel: Idempotenz nicht als einzelne Assertion behandeln, sondern als wiederholbaren Replay-Vertrag mit sichtbarer Effektzählung.

Lab 37 Serialization Round-Trip Contract Objekt in eine kanonische Form schreiben, wieder lesen und die fachlich relevanten Werte als stabilen Vertrag prüfen.

Serialization Round-Trip Contract

Code Smell

Serialisierung wird nur per Stringvergleich geprüft; Feldreihenfolge, Escaping und Rücklesbarkeit bleiben ungeschützt.

Pattern / Prinzip

Round-Trip Contract + Canonical Form

Testfokus

Objekt in eine kanonische Form schreiben, wieder lesen und die fachlich relevanten Werte als stabilen Vertrag prüfen.

Alternative

Golden Master, wenn eine große bestehende Ausgabe zunächst unverändert abgesichert werden muss.

Vor dem Refactoring

schwacher Testvertrag
// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.

Referenzlösung

SerializationRoundTripContract.java
package com.aydinsude.workbench.testing;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

// Testing Pattern: Round-Trip Contract + Canonical Form
// Zweck: Fachwerte müssen Serialisierung und Deserialisierung verlustfrei überstehen.
public final class SerializationRoundTripContract {
  public record OrderView(String id, int quantity, String status) {}

  public String write(OrderView value) {
    Objects.requireNonNull(value);
    return Map.of("id", value.id(), "quantity", Integer.toString(value.quantity()), "status", value.status())
        .entrySet().stream().sorted(Map.Entry.comparingByKey())
        .map(e -> escape(e.getKey()) + "=" + escape(e.getValue()))
        .collect(Collectors.joining(";"));
  }

  public OrderView read(String text) {
    Map<String, String> fields = new LinkedHashMap<>();
    for (String part : text.split(";")) {
      String[] pair = part.split("=", 2);
      fields.put(unescape(pair[0]), unescape(pair[1]));
    }
    return new OrderView(fields.get("id"), Integer.parseInt(fields.get("quantity")), fields.get("status"));
  }

  public boolean roundTrips(OrderView value) { return value.equals(read(write(value))); }
  private String escape(String s) { return s.replace("%", "%25").replace(";", "%3B").replace("=", "%3D"); }
  private String unescape(String s) { return s.replace("%3D", "=").replace("%3B", ";").replace("%25", "%"); }
}

JUnit-Test

SerializationRoundTripContractTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class SerializationRoundTripContractTest {
  @Test void preserves_semantic_values_including_reserved_characters() {
    var contract = new SerializationRoundTripContract();
    var value = new SerializationRoundTripContract.OrderView("O=17;EU", 3, "READY");
    assertTrue(contract.roundTrips(value));
    assertEquals(value, contract.read(contract.write(value)));
  }
}
Refactoring-Regel: Nicht das zufällige Textlayout testen, sondern die reversible fachliche Bedeutung der Serialisierung.

Lab 38 Retry Policy Contract Eine explizite Retry-Policy mit skriptbaren Fehlerfolgen testen und permanente Fehler sicher vom Retry ausschließen.

Retry Policy Contract

Code Smell

Retry-Tests prüfen nur die Aufrufzahl; Fehlerklassifizierung, Abbruch und Erfolg nach transientem Fehler bleiben unklar.

Pattern / Prinzip

Policy Object + Scripted Failure

Testfokus

Eine explizite Retry-Policy mit skriptbaren Fehlerfolgen testen und permanente Fehler sicher vom Retry ausschließen.

Alternative

Fault Injection Harness, wenn ein ganzer Recovery-Workflow mit mehreren Komponenten geprüft werden soll.

Vor dem Refactoring

schwacher Testvertrag
// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.

Referenzlösung

RetryPolicyContract.java
package com.aydinsude.workbench.testing;

import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

// Testing Pattern: Policy Object + Scripted Failure
// Zweck: Retry-Entscheidungen anhand expliziter Fehlerklassen reproduzierbar prüfen.
public final class RetryPolicyContract {
  public enum FailureKind { TRANSIENT, PERMANENT }
  public record AttemptResult(boolean success, FailureKind failure) {}
  public record Outcome(boolean success, int attempts, FailureKind lastFailure) {}

  public Outcome execute(int maxAttempts, Supplier<AttemptResult> operation) {
    if (maxAttempts < 1) throw new IllegalArgumentException("maxAttempts must be positive");
    Objects.requireNonNull(operation);
    FailureKind last = null;
    for (int attempt = 1; attempt <= maxAttempts; attempt++) {
      AttemptResult result = operation.get();
      if (result.success()) return new Outcome(true, attempt, last);
      last = result.failure();
      if (last == FailureKind.PERMANENT) return new Outcome(false, attempt, last);
    }
    return new Outcome(false, maxAttempts, last);
  }

  public static Supplier<AttemptResult> script(List<AttemptResult> results) {
    var iterator = results.iterator();
    return () -> iterator.hasNext() ? iterator.next() : results.getLast();
  }
}

JUnit-Test

RetryPolicyContractTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;

class RetryPolicyContractTest {
  @Test void retries_transient_failure_but_stops_on_success() {
    var policy = new RetryPolicyContract();
    var outcome = policy.execute(4, RetryPolicyContract.script(List.of(
        new RetryPolicyContract.AttemptResult(false, RetryPolicyContract.FailureKind.TRANSIENT),
        new RetryPolicyContract.AttemptResult(true, null))));
    assertTrue(outcome.success());
    assertEquals(2, outcome.attempts());
  }

  @Test void permanent_failure_is_not_retried() {
    var policy = new RetryPolicyContract();
    var outcome = policy.execute(5, RetryPolicyContract.script(List.of(
        new RetryPolicyContract.AttemptResult(false, RetryPolicyContract.FailureKind.PERMANENT))));
    assertFalse(outcome.success());
    assertEquals(1, outcome.attempts());
  }
}
Refactoring-Regel: Retry als fachlich-technische Policy mit Fehlerklassen, Maximalversuchen und Abbruchgrund testen.

Lab 39 Differential Test Oracle Legacy- und Zielimplementierung mit denselben Fällen ausführen, Abweichungen normalisieren und als präzise Migrationsdifferenz ausgeben.

Differential Test Oracle

Code Smell

Bei Legacy-Migrationen wird die neue Implementierung isoliert getestet; semantische Abweichungen zur Altlogik bleiben unentdeckt.

Pattern / Prinzip

Differential Testing + Dual Run

Testfokus

Legacy- und Zielimplementierung mit denselben Fällen ausführen, Abweichungen normalisieren und als präzise Migrationsdifferenz ausgeben.

Alternative

Characterization Test, wenn nur der bestehende Zustand eingefroren und noch keine Zielimplementierung verglichen wird.

Vor dem Refactoring

schwacher Testvertrag
// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.

Referenzlösung

DifferentialTestOracle.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.UnaryOperator;

// Testing Pattern: Differential Testing + Dual Run
// Zweck: Legacy- und Zielimplementierung mit identischen Fällen semantisch vergleichen.
public final class DifferentialTestOracle<T, R> {
  public record Difference<T, R>(T input, R legacy, R target) {}

  public List<Difference<T, R>> compare(
      List<T> cases,
      Function<T, R> legacy,
      Function<T, R> target,
      UnaryOperator<R> normalizer) {
    Objects.requireNonNull(cases);
    var differences = new ArrayList<Difference<T, R>>();
    for (T input : cases) {
      R oldValue = legacy.apply(input);
      R newValue = target.apply(input);
      if (!Objects.equals(normalizer.apply(oldValue), normalizer.apply(newValue))) {
        differences.add(new Difference<>(input, oldValue, newValue));
      }
    }
    return List.copyOf(differences);
  }
}

JUnit-Test

DifferentialTestOracleTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;

class DifferentialTestOracleTest {
  @Test void reports_only_semantic_migration_difference() {
    var oracle = new DifferentialTestOracle<Integer, String>();
    var differences = oracle.compare(List.of(1, 2, 3),
        n -> n == 3 ? "REJECTED" : "accepted",
        n -> n == 3 ? "ACCEPTED" : "ACCEPTED",
        String::toUpperCase);
    assertEquals(1, differences.size());
    assertEquals(3, differences.getFirst().input());
  }
}
Refactoring-Regel: Bei schrittweiser Migration beide Implementierungen mit identischen Eingaben vergleichen und erlaubte Differenzen explizit modellieren.

Lab 40 Temporal Coupling Scenario Erlaubte Schritte als kleine Scenario-DSL und explizite Zustandsmaschine formulieren, sodass Reihenfolgeverträge lesbar und testbar werden.

Temporal Coupling Scenario

Code Smell

Tests rufen Methoden in impliziter Reihenfolge auf; bei falscher Reihenfolge entstehen zufällige Exceptions ohne fachliche Erklärung.

Pattern / Prinzip

Scenario DSL + Explicit State Machine

Testfokus

Erlaubte Schritte als kleine Scenario-DSL und explizite Zustandsmaschine formulieren, sodass Reihenfolgeverträge lesbar und testbar werden.

Alternative

State Transition Matrix, wenn viele Zustände und Übergänge tabellarisch vollständig geprüft werden müssen.

Vor dem Refactoring

schwacher Testvertrag
// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.

Referenzlösung

TemporalCouplingScenario.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;

// Testing Pattern: Scenario DSL + Explicit State Machine
// Zweck: Zeitliche Kopplung als lesbaren, ausführbaren Testvertrag modellieren.
public final class TemporalCouplingScenario {
  public enum State { NEW, RESERVED, PAID, COMPLETED }
  private State state = State.NEW;
  private final List<String> history = new ArrayList<>();

  public TemporalCouplingScenario reserve() { require(State.NEW, "reserve"); state = State.RESERVED; history.add("RESERVED"); return this; }
  public TemporalCouplingScenario pay() { require(State.RESERVED, "pay"); state = State.PAID; history.add("PAID"); return this; }
  public TemporalCouplingScenario complete() { require(State.PAID, "complete"); state = State.COMPLETED; history.add("COMPLETED"); return this; }

  public State state() { return state; }
  public List<String> history() { return List.copyOf(history); }

  private void require(State expected, String action) {
    if (state != expected) throw new IllegalStateException(action + " requires " + expected + " but was " + state);
  }
}

JUnit-Test

TemporalCouplingScenarioTest.java
package com.aydinsude.workbench.testing;

import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;

class TemporalCouplingScenarioTest {
  @Test void scenario_expresses_valid_business_order() {
    var scenario = new TemporalCouplingScenario().reserve().pay().complete();
    assertEquals(TemporalCouplingScenario.State.COMPLETED, scenario.state());
    assertEquals(List.of("RESERVED", "PAID", "COMPLETED"), scenario.history());
  }

  @Test void invalid_order_has_precise_contract_failure() {
    var error = assertThrows(IllegalStateException.class,
        () -> new TemporalCouplingScenario().pay());
    assertTrue(error.getMessage().contains("requires RESERVED"));
  }
}
Refactoring-Regel: Zeitliche Kopplung sichtbar machen: Schritte benennen, Zustand explizit führen und illegale Reihenfolgen mit fachlichem Fehler ablehnen.
Kapitel 9 Scheduler, Zeitsteuerung und deterministische Abläufe5 Praxislabore

Fünf Labs machen Zeitsteuerung, Eventual Consistency, Autorisierung, Rückwärtskompatibilität und sichere Testauswahl als reproduzierbare Verträge sichtbar.

Lab 41 Deterministic Scheduler Harness Zeitgesteuerte Aufgaben ohne echte Wartezeit ausführen und Reihenfolge, Fälligkeit sowie Wiederholungen deterministisch prüfen.

Deterministic Scheduler Harness

Code Smell

Echte Scheduler, Sleeps und Wanduhren machen Tests langsam und flüchtig.

Pattern / Prinzip

Manual Scheduler + Virtual Time

Testfokus

Zeitgesteuerte Aufgaben ohne echte Wartezeit ausführen und Reihenfolge, Fälligkeit sowie Wiederholungen deterministisch prüfen.

Alternative

Eventually Probe, wenn nur ein beobachtbares Ergebnis und nicht der Zeitplan selbst relevant ist.

Vor dem Refactoring

schwacher Testvertrag
// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

DeterministicSchedulerHarness.java
package com.aydinsude.workbench.testing;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

// Testing Pattern: Manual Scheduler + Virtual Time
// Zweck: Zeitgesteuerte Abläufe ohne echte Wartezeit reproduzierbar prüfen.
public final class DeterministicSchedulerHarness {
  private record Task(Instant dueAt, String name, Runnable action) {}
  private final List<Task> tasks = new ArrayList<>();
  private final List<String> executed = new ArrayList<>();
  private Instant now;

  public DeterministicSchedulerHarness(Instant start) { this.now = Objects.requireNonNull(start); }
  public void schedule(String name, Duration delay, Runnable action) {
    tasks.add(new Task(now.plus(delay), name, action));
  }
  public void advance(Duration duration) {
    now = now.plus(duration);
    tasks.stream().filter(t -> !t.dueAt().isAfter(now)).sorted(Comparator.comparing(Task::dueAt)).toList()
        .forEach(t -> { t.action().run(); executed.add(t.name()); tasks.remove(t); });
  }
  public List<String> executed() { return List.copyOf(executed); }
  public Instant now() { return now; }
}

JUnit-Test

DeterministicSchedulerHarnessTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import java.time.*;
import org.junit.jupiter.api.Test;
class DeterministicSchedulerHarnessTest {
 @Test void executes_only_due_tasks_in_order() {
  var h=new DeterministicSchedulerHarness(Instant.parse("2026-01-01T00:00:00Z"));
  var seen=new java.util.ArrayList<String>();
  h.schedule("later",Duration.ofMinutes(10),()->seen.add("later"));
  h.schedule("first",Duration.ofMinutes(2),()->seen.add("first"));
  h.advance(Duration.ofMinutes(3));
  assertEquals(java.util.List.of("first"),seen);
  assertEquals(java.util.List.of("first"),h.executed());
 }
}
Refactoring-Regel: Zeitgesteuerte Aufgaben ohne echte Wartezeit ausführen und Reihenfolge, Fälligkeit sowie Wiederholungen deterministisch prüfen.

Lab 42 Eventual Consistency Scenario Schreib- und Lesemodell mit expliziten Projektionsschritten testen, ohne zufälliges Polling oder versteckte Threads.

Eventual Consistency Scenario

Code Smell

Tests hoffen, dass ein Read Model irgendwann aktualisiert ist, und kaschieren Race Conditions mit Sleeps.

Pattern / Prinzip

Scenario Harness + Explicit Projection Step

Testfokus

Schreib- und Lesemodell mit expliziten Projektionsschritten testen, ohne zufälliges Polling oder versteckte Threads.

Alternative

Async Event Probe, wenn die reale asynchrone Infrastruktur bewusst Teil des Integrationstests ist.

Vor dem Refactoring

schwacher Testvertrag
// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

EventualConsistencyScenario.java
package com.aydinsude.workbench.testing;

import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

// Testing Pattern: Scenario Harness + Explicit Projection Step
// Zweck: Eventual Consistency als kontrollierte Abfolge von Write, Event und Projection prüfen.
public final class EventualConsistencyScenario {
  private final Queue<String> events = new ArrayDeque<>();
  private final Map<String, String> readModel = new HashMap<>();

  public void write(String id, String status) { events.add(id + ":" + status); }
  public boolean visible(String id) { return readModel.containsKey(id); }
  public void projectNext() {
    String event = events.remove();
    String[] parts = event.split(":", 2);
    readModel.put(parts[0], parts[1]);
  }
  public String status(String id) { return readModel.get(id); }
  public int pendingEvents() { return events.size(); }
}

JUnit-Test

EventualConsistencyScenarioTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class EventualConsistencyScenarioTest {
 @Test void read_model_changes_only_after_projection_step() {
  var s=new EventualConsistencyScenario();
  s.write("O-7","APPROVED");
  assertFalse(s.visible("O-7"));
  assertEquals(1,s.pendingEvents());
  s.projectNext();
  assertEquals("APPROVED",s.status("O-7"));
 }
}
Refactoring-Regel: Schreib- und Lesemodell mit expliziten Projektionsschritten testen, ohne zufälliges Polling oder versteckte Threads.

Lab 43 Authorization Matrix Contract Rollen, Aktionen und Ressourcen als vollständige Entscheidungsmatrix testen und fehlende Regeln als Fehler behandeln.

Authorization Matrix Contract

Code Smell

Sicherheitsregeln werden nur durch einzelne Happy-Path-Tests geprüft; negative Fälle und neue Aktionen bleiben offen.

Pattern / Prinzip

Decision Table + Deny by Default

Testfokus

Rollen, Aktionen und Ressourcen als vollständige Entscheidungsmatrix testen und fehlende Regeln als Fehler behandeln.

Alternative

Property-Based Cases, wenn sehr viele Rollen- und Ressourcenattribute kombiniert werden.

Vor dem Refactoring

schwacher Testvertrag
// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

AuthorizationMatrixContract.java
package com.aydinsude.workbench.testing;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

// Testing Pattern: Decision Table + Deny by Default
// Zweck: Autorisierung als vollständigen fachlichen Vertrag statt als verstreute Einzelassertion prüfen.
public final class AuthorizationMatrixContract {
  public record Key(String role, String action, String resource) {
    public Key { Objects.requireNonNull(role); Objects.requireNonNull(action); Objects.requireNonNull(resource); }
  }
  private final Map<Key, Boolean> decisions = new HashMap<>();
  public AuthorizationMatrixContract allow(String role, String action, String resource) {
    decisions.put(new Key(role, action, resource), true); return this;
  }
  public AuthorizationMatrixContract deny(String role, String action, String resource) {
    decisions.put(new Key(role, action, resource), false); return this;
  }
  public boolean permitted(String role, String action, String resource) {
    return decisions.getOrDefault(new Key(role, action, resource), false);
  }
  public int explicitRules() { return decisions.size(); }
}

JUnit-Test

AuthorizationMatrixContractTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class AuthorizationMatrixContractTest {
 @Test void denies_unknown_combinations_by_default() {
  var c=new AuthorizationMatrixContract().allow("ADMIN","READ","CLAIM").deny("AGENT","DELETE","CLAIM");
  assertTrue(c.permitted("ADMIN","READ","CLAIM"));
  assertFalse(c.permitted("AGENT","DELETE","CLAIM"));
  assertFalse(c.permitted("ADMIN","EXPORT","CLAIM"));
 }
}
Refactoring-Regel: Rollen, Aktionen und Ressourcen als vollständige Entscheidungsmatrix testen und fehlende Regeln als Fehler behandeln.

Lab 44 Backward-Compatible Fixture Contract Alte und neue Payload-Versionen gegen denselben fachlichen Decoder prüfen und Pflichtwerte explizit absichern.

Backward-Compatible Fixture Contract

Code Smell

Tests verwenden nur die aktuelle Payload; alte Felder, Umbenennungen und additive Änderungen werden unbemerkt gebrochen.

Pattern / Prinzip

Compatibility Fixture + Tolerant Reader

Testfokus

Alte und neue Payload-Versionen gegen denselben fachlichen Decoder prüfen und Pflichtwerte explizit absichern.

Alternative

Consumer-Driven Contract, wenn externe Consumer ihre Erwartungen unabhängig veröffentlichen.

Vor dem Refactoring

schwacher Testvertrag
// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

BackwardCompatibleFixtureContract.java
package com.aydinsude.workbench.testing;

import java.util.Map;

// Testing Pattern: Compatibility Fixture + Tolerant Reader
// Zweck: Mehrere historische Payload-Formen auf dasselbe stabile Fachmodell abbilden.
public final class BackwardCompatibleFixtureContract {
  public record Customer(String id, String displayName, String tier) {}

  public Customer decode(Map<String, String> payload) {
    String id = required(payload, "id");
    String name = payload.containsKey("displayName") ? payload.get("displayName") : required(payload, "name");
    String tier = payload.getOrDefault("tier", "STANDARD");
    return new Customer(id, name, tier);
  }
  public boolean compatible(Map<String,String> payload) {
    try { decode(payload); return true; } catch (IllegalArgumentException ex) { return false; }
  }
  private String required(Map<String,String> payload, String key) {
    String value=payload.get(key); if(value==null||value.isBlank()) throw new IllegalArgumentException("missing "+key); return value;
  }
}

JUnit-Test

BackwardCompatibleFixtureContractTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;
class BackwardCompatibleFixtureContractTest {
 @Test void accepts_legacy_and_current_payloads() {
  var c=new BackwardCompatibleFixtureContract();
  assertEquals("Ada",c.decode(Map.of("id","C1","name","Ada")).displayName());
  assertEquals("GOLD",c.decode(Map.of("id","C1","displayName","Ada","tier","GOLD")).tier());
  assertFalse(c.compatible(Map.of("displayName","Ada")));
 }
}
Refactoring-Regel: Alte und neue Payload-Versionen gegen denselben fachlichen Decoder prüfen und Pflichtwerte explizit absichern.

Lab 45 Test Impact Selector Geänderte Komponenten auf relevante Test-Suites abbilden und bei unbekannten Änderungen bewusst auf den sicheren Volltest zurückfallen.

Test Impact Selector

Code Smell

Testauswahl basiert auf Dateinamen oder Bauchgefühl; wichtige Regressionstests werden bei Querschnittsänderungen ausgelassen.

Pattern / Prinzip

Dependency Map + Safe Fallback

Testfokus

Geänderte Komponenten auf relevante Test-Suites abbilden und bei unbekannten Änderungen bewusst auf den sicheren Volltest zurückfallen.

Alternative

Architecture Fitness Rule, wenn nur verbotene Abhängigkeiten und nicht Testauswahl abgesichert werden sollen.

Vor dem Refactoring

schwacher Testvertrag
// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.

Referenzlösung

TestImpactSelector.java
package com.aydinsude.workbench.testing;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

// Testing Pattern: Dependency Map + Safe Fallback
// Zweck: Relevante Tests deterministisch wählen, bei unbekannter Auswirkung aber nie still zu wenig testen.
public final class TestImpactSelector {
  private final Map<String, Set<String>> suitesByComponent = new LinkedHashMap<>();
  private final Set<String> fullSuite;

  public TestImpactSelector(Set<String> fullSuite) { this.fullSuite = Set.copyOf(fullSuite); }
  public TestImpactSelector register(String component, String... suites) {
    suitesByComponent.put(component, Set.of(suites)); return this;
  }
  public Set<String> select(Set<String> changedComponents) {
    Set<String> selected = new LinkedHashSet<>();
    for (String component : changedComponents) {
      Set<String> suites = suitesByComponent.get(component);
      if (suites == null) return fullSuite;
      selected.addAll(suites);
    }
    return Set.copyOf(selected);
  }
}

JUnit-Test

TestImpactSelectorTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Set;
import org.junit.jupiter.api.Test;
class TestImpactSelectorTest {
 @Test void selects_known_suites_and_falls_back_safely() {
  var selector=new TestImpactSelector(Set.of("all-unit","all-integration"))
      .register("pricing","pricing-unit","order-contract")
      .register("orders","order-unit","order-contract");
  assertEquals(Set.of("pricing-unit","order-contract"),selector.select(Set.of("pricing")));
  assertEquals(Set.of("all-unit","all-integration"),selector.select(Set.of("shared-kernel")));
 }
}
Refactoring-Regel: Geänderte Komponenten auf relevante Test-Suites abbilden und bei unbekannten Änderungen bewusst auf den sicheren Volltest zurückfallen.
Kapitel 10 Testpyramide, Suite-Budget und Ausführungsstrategie5 Praxislabore

Die letzten fünf Labs schließen den Testing-Bereich mit Suite-Portfolio, Parallelisolation, Datenschutz, Recovery und einer evidenzbasierten Release-Entscheidung ab.

Lab 46 Test Suite Layer Budget Testportfolio nach Unit, Component, Integration und End-to-End klassifizieren und Budgetverletzungen früh sichtbar machen.

Test Suite Layer Budget

Code Smell

Die Suite wächst zufällig: zu viele langsame End-to-End-Tests, doppelte Szenarien und keine sichtbare Verteilung nach Testebene.

Pattern / Prinzip

Portfolio Test + Layer Budget

Testfokus

Testportfolio nach Unit, Component, Integration und End-to-End klassifizieren und Budgetverletzungen früh sichtbar machen.

Alternative

Test Impact Selection, wenn die Testebenen bereits sauber sind und nur die Auswahl pro Änderung optimiert werden soll.

Vor dem Refactoring

schwacher Testvertrag
// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.

Referenzlösung

TestSuiteLayerBudget.java
package com.aydinsude.workbench.testing;

import java.util.EnumMap;
import java.util.Map;

// Testing Pattern: Portfolio Test + Layer Budget
// Zweck: Die Testpyramide als ausführbare Qualitätsregel statt als unverbindliche Grafik behandeln.
public final class TestSuiteLayerBudget {
  public enum Layer { UNIT, COMPONENT, INTEGRATION, END_TO_END }
  private final Map<Layer, Integer> limits = new EnumMap<>(Layer.class);
  private final Map<Layer, Integer> actual = new EnumMap<>(Layer.class);

  public TestSuiteLayerBudget limit(Layer layer, int maximum) {
    limits.put(layer, maximum); return this;
  }
  public TestSuiteLayerBudget record(Layer layer) {
    actual.merge(layer, 1, Integer::sum); return this;
  }
  public boolean withinBudget() {
    return limits.entrySet().stream().allMatch(e -> actual.getOrDefault(e.getKey(), 0) <= e.getValue());
  }
  public Map<Layer, Integer> violations() {
    Map<Layer, Integer> result = new EnumMap<>(Layer.class);
    limits.forEach((layer, max) -> {
      int excess = actual.getOrDefault(layer, 0) - max;
      if (excess > 0) result.put(layer, excess);
    });
    return Map.copyOf(result);
  }
}

JUnit-Test

TestSuiteLayerBudgetTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class TestSuiteLayerBudgetTest {
 @Test void reports_excessive_end_to_end_tests() {
  var budget=new TestSuiteLayerBudget().limit(TestSuiteLayerBudget.Layer.END_TO_END,1);
  budget.record(TestSuiteLayerBudget.Layer.END_TO_END).record(TestSuiteLayerBudget.Layer.END_TO_END);
  assertFalse(budget.withinBudget());
  assertEquals(1,budget.violations().get(TestSuiteLayerBudget.Layer.END_TO_END));
 }
}
Refactoring-Regel: Testportfolio nach Unit, Component, Integration und End-to-End klassifizieren und Budgetverletzungen früh sichtbar machen.

Lab 47 Parallel Test Isolation Jeden Testlauf mit einem eindeutigen Namespace und explizitem Lebenszyklus isolieren.

Parallel Test Isolation

Code Smell

Parallel laufende Tests teilen feste IDs, Tabellenzeilen, Ports oder Verzeichnisse und beeinflussen sich gegenseitig.

Pattern / Prinzip

Isolated Namespace + Lease

Testfokus

Jeden Testlauf mit einem eindeutigen Namespace und explizitem Lebenszyklus isolieren.

Alternative

Serialisierung der Tests nur als kurzfristige Übergangslösung, wenn die Infrastruktur noch keine Isolation unterstützt.

Vor dem Refactoring

schwacher Testvertrag
// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.

Referenzlösung

ParallelTestIsolation.java
package com.aydinsude.workbench.testing;

import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

// Testing Pattern: Isolated Namespace + Lease
// Zweck: Parallele Tests erhalten getrennte Ressourcen und geben sie kontrolliert frei.
public final class ParallelTestIsolation {
  public record Lease(String namespace) implements AutoCloseable {
    @Override public void close() { ACTIVE.remove(namespace); }
  }
  private static final Set<String> ACTIVE = java.util.Collections.synchronizedSet(new HashSet<>());

  public Lease acquire(String testName) {
    String namespace = testName.replaceAll("[^a-zA-Z0-9]", "-") + "-" + UUID.randomUUID();
    if (!ACTIVE.add(namespace)) throw new IllegalStateException("duplicate namespace");
    return new Lease(namespace);
  }
  public boolean active(String namespace) { return ACTIVE.contains(namespace); }
}

JUnit-Test

ParallelTestIsolationTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class ParallelTestIsolationTest {
 @Test void allocates_distinct_namespaces_and_releases_them() {
  var isolation=new ParallelTestIsolation();
  String first;
  try(var a=isolation.acquire("order test"); var b=isolation.acquire("order test")) {
   first=a.namespace(); assertNotEquals(a.namespace(),b.namespace()); assertTrue(isolation.active(first));
  }
  assertFalse(isolation.active(first));
 }
}
Refactoring-Regel: Jeden Testlauf mit einem eindeutigen Namespace und explizitem Lebenszyklus isolieren.

Lab 48 Sensitive Test Data Boundary Nur synthetische, minimale und klassifizierte Testdaten zulassen und sensible Felder vor Speicherung ablehnen.

Sensitive Test Data Boundary

Code Smell

Tests kopieren produktionsnahe personenbezogene Daten, Tokens oder vollständige Payloads in Fixtures und Snapshots.

Pattern / Prinzip

Synthetic Fixture + Data Minimization

Testfokus

Nur synthetische, minimale und klassifizierte Testdaten zulassen und sensible Felder vor Speicherung ablehnen.

Alternative

Tokenisierung durch einen externen Testdatendienst, wenn realistische Datenverteilungen zwingend benötigt werden.

Vor dem Refactoring

schwacher Testvertrag
// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.

Referenzlösung

SensitiveTestDataBoundary.java
package com.aydinsude.workbench.testing;

import java.util.Map;
import java.util.Set;

// Testing Pattern: Synthetic Fixture + Data Minimization
// Zweck: Test-Fixtures dürfen keine produktionsnahen Geheimnisse oder unnötigen Personendaten enthalten.
public final class SensitiveTestDataBoundary {
  private static final Set<String> FORBIDDEN = Set.of("password", "token", "iban", "socialSecurityNumber");

  public Map<String, String> accept(Map<String, String> fixture) {
    var violations = fixture.keySet().stream().filter(FORBIDDEN::contains).sorted().toList();
    if (!violations.isEmpty()) throw new IllegalArgumentException("sensitive fields: " + violations);
    if (!"synthetic".equals(fixture.get("dataOrigin"))) {
      throw new IllegalArgumentException("fixture must declare synthetic origin");
    }
    return Map.copyOf(fixture);
  }
}

JUnit-Test

SensitiveTestDataBoundaryTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import java.util.Map;
import org.junit.jupiter.api.Test;
class SensitiveTestDataBoundaryTest {
 @Test void rejects_sensitive_fields() {
  var boundary=new SensitiveTestDataBoundary();
  var ex=assertThrows(IllegalArgumentException.class,()->boundary.accept(Map.of("dataOrigin","synthetic","token","secret")));
  assertTrue(ex.getMessage().contains("token"));
 }
}
Refactoring-Regel: Nur synthetische, minimale und klassifizierte Testdaten zulassen und sensible Felder vor Speicherung ablehnen.

Lab 49 Recovery Scenario Harness Fehlerpunkte skripten, Neustart simulieren und fachliche Recovery-Invarianten explizit prüfen.

Recovery Scenario Harness

Code Smell

Tests prüfen nur den Normalfall; Crash, Wiederanlauf, doppelte Zustellung und teilweise Persistenz bleiben unbewiesen.

Pattern / Prinzip

Failure Script + Recovery Oracle

Testfokus

Fehlerpunkte skripten, Neustart simulieren und fachliche Recovery-Invarianten explizit prüfen.

Alternative

Fault Injection Harness, wenn einzelne technische Fehler statt eines vollständigen Wiederanlaufszenarios untersucht werden.

Vor dem Refactoring

schwacher Testvertrag
// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.

Referenzlösung

RecoveryScenarioHarness.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;

// Testing Pattern: Failure Script + Recovery Oracle
// Zweck: Crash und Wiederanlauf als deterministisches fachliches Szenario testen.
public final class RecoveryScenarioHarness {
  private final List<String> durableLog = new ArrayList<>();
  private boolean failAfterPrepare;

  public RecoveryScenarioHarness failAfterPrepare() { failAfterPrepare = true; return this; }
  public void execute(String commandId) {
    if (!durableLog.contains("PREPARED:" + commandId)) durableLog.add("PREPARED:" + commandId);
    if (failAfterPrepare) { failAfterPrepare = false; throw new SimulatedCrash(); }
    commit(commandId);
  }
  public void recover(String commandId) { commit(commandId); }
  private void commit(String commandId) {
    if (!durableLog.contains("COMMITTED:" + commandId)) durableLog.add("COMMITTED:" + commandId);
  }
  public long commits(String commandId) { return durableLog.stream().filter(e -> e.equals("COMMITTED:" + commandId)).count(); }
  public static final class SimulatedCrash extends RuntimeException {}
}

JUnit-Test

RecoveryScenarioHarnessTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class RecoveryScenarioHarnessTest {
 @Test void recovery_commits_exactly_once_after_crash() {
  var h=new RecoveryScenarioHarness().failAfterPrepare();
  assertThrows(RecoveryScenarioHarness.SimulatedCrash.class,()->h.execute("C-1"));
  h.recover("C-1"); h.recover("C-1");
  assertEquals(1,h.commits("C-1"));
 }
}
Refactoring-Regel: Fehlerpunkte skripten, Neustart simulieren und fachliche Recovery-Invarianten explizit prüfen.

Lab 50 Release Confidence Gate Heterogene Testevidenz gewichten und eine nachvollziehbare Freigabeentscheidung mit konkreten Blockern erzeugen.

Release Confidence Gate

Code Smell

Freigaben beruhen auf einem grünen Gesamtstatus, obwohl kritische Verträge, Mutationsergebnis oder Architekturregeln fehlen.

Pattern / Prinzip

Quality Gate + Evidence Aggregator

Testfokus

Heterogene Testevidenz gewichten und eine nachvollziehbare Freigabeentscheidung mit konkreten Blockern erzeugen.

Alternative

Ein einfaches CI-Pass/Fail reicht nur bei kleinen Systemen ohne unterschiedliche Risikoklassen.

Vor dem Refactoring

schwacher Testvertrag
// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.

Referenzlösung

ReleaseConfidenceGate.java
package com.aydinsude.workbench.testing;

import java.util.ArrayList;
import java.util.List;

// Testing Pattern: Quality Gate + Evidence Aggregator
// Zweck: Eine Release-Entscheidung wird aus benannter, risikogewichteter Testevidenz abgeleitet.
public final class ReleaseConfidenceGate {
  public enum Criticality { REQUIRED, ADVISORY }
  public record Evidence(String name, Criticality criticality, boolean passed) {}
  public record Decision(boolean releasable, List<String> blockers, List<String> warnings) {}

  private final List<Evidence> evidence = new ArrayList<>();
  public ReleaseConfidenceGate add(String name, Criticality criticality, boolean passed) {
    evidence.add(new Evidence(name, criticality, passed)); return this;
  }
  public Decision decide() {
    var blockers=evidence.stream().filter(e->!e.passed()&&e.criticality()==Criticality.REQUIRED).map(Evidence::name).sorted().toList();
    var warnings=evidence.stream().filter(e->!e.passed()&&e.criticality()==Criticality.ADVISORY).map(Evidence::name).sorted().toList();
    return new Decision(blockers.isEmpty(), blockers, warnings);
  }
}

JUnit-Test

ReleaseConfidenceGateTest.java
package com.aydinsude.workbench.testing;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class ReleaseConfidenceGateTest {
 @Test void blocks_release_only_for_required_failed_evidence() {
  var decision=new ReleaseConfidenceGate()
    .add("contract-tests",ReleaseConfidenceGate.Criticality.REQUIRED,false)
    .add("performance-trend",ReleaseConfidenceGate.Criticality.ADVISORY,false).decide();
  assertFalse(decision.releasable());
  assertEquals(java.util.List.of("contract-tests"),decision.blockers());
  assertEquals(java.util.List.of("performance-trend"),decision.warnings());
 }
}
Refactoring-Regel: Heterogene Testevidenz gewichten und eine nachvollziehbare Freigabeentscheidung mit konkreten Blockern erzeugen.
⌂ Cockpit