Verhalten sichern
Characterization Tests und Golden Master vor strukturellen Änderungen einsetzen.
Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.
Grundlage → Entscheidungskriterien → Refactoring-Pfad → ausführbare Referenz → Einsatzgrenzen
Die vorhandenen Kapitel bleiben vollständig erhalten. Diese Orientierung gruppiert sie nach den Entscheidungen, die ein Enterprise-Team tatsächlich treffen muss.
Characterization Tests und Golden Master vor strukturellen Änderungen einsetzen.
Decision Tables, Parameterisierung und Value-Object-Tests für Invarianten nutzen.
Fakes, Stubs und Contract Tests an Ports statt an Implementierungsdetails ausrichten.
Nach jedem Refactoring-Schritt einen kleinen, aussagekräftigen Testlauf ermöglichen.
Fünf grundlegende Testrefactorings zeigen, wie Tests Verhalten absichern, Refactorings ermöglichen und nicht an Implementierungsdetails koppeln.
Legacy-Berechnung wird refaktoriert, obwohl ihr tatsächliches Verhalten nicht abgesichert ist.
Characterization Test + Golden Master
Stabile Beobachtungspunkte, Randfälle und absichtliche Altfehler dokumentieren.
Direkte Unit Tests, wenn fachliche Regeln bereits eindeutig dokumentiert sind.
fragiles Testdesign// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.CharacterizationTestBoundary.javapackage 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());
}
}
Tests erzeugen große Fachobjekte mit unlesbaren Konstruktoren und zufälligen Standardwerten.
Builder + Object Mother Boundary
Builder-Defaults müssen gültig, deterministisch und fachlich neutral sein.
Factory Methods für wenige klar benannte Standardszenarien.
fragiles Testdesign// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.OrderTestDataBuilder.javapackage 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); }
}
Dieselbe fachliche Regel wird in vielen fast identischen Tests kopiert und driftet auseinander.
Decision Table + Parameterized Test
Fallnamen, Grenzwerte und erwartete Begründungen sind Teil des Testvertrags.
Einzeltests, wenn jeder Fall einen deutlich anderen Ablauf oder eine eigene Erklärung benötigt.
fragiles Testdesign// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.DiscountDecisionTable.javapackage 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));
}
}
Übermockte Tests prüfen Interaktionsdetails statt fachliches Verhalten und brechen bei jeder Umstrukturierung.
Fake + Repository Contract
Fake und Produktionsadapter müssen denselben Contract-Test bestehen.
Mock für echte Protokollgrenzen, wenn Reihenfolge oder exakt ein Aufruf Vertragsbestandteil ist.
fragiles Testdesign// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.CustomerRepositoryFake.javapackage 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(); }
}
Mehrere Adapter behaupten denselben Port zu implementieren, werden aber mit unterschiedlichen Erwartungen getestet.
Contract Test + Test Template
Vertrag testet beobachtbares Verhalten, nicht interne Datenstrukturen des Adapters.
End-to-End-Test, wenn die Aussage nur im vollständig integrierten System möglich ist.
fragiles Testdesign// Test kennt interne Aufrufreihenfolge, private Helfer oder zufällige Fixtures.
// Eine harmlose Umstrukturierung bricht den Test, obwohl das Verhalten gleich bleibt.PaymentGatewayContractKit.javapackage 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");
}
}
Fünf Labs entfernen Zeit-, Identitäts-, Ausgangs- und Nebenläufigkeits-Zufall aus Tests und erweitern Beispieltests zu reproduzierbaren Invarianten.
Tests verwenden Instant.now() oder LocalDate.now() direkt und werden zeitabhängig, langsam oder flakey.
Clock Port + Dependency Injection
Grenzzeitpunkte, Zeitzonen und exakt reproduzierbare Ablaufentscheidungen.
Feste Zeitparameter direkt an reine Funktionen übergeben, wenn kein laufender Zeitbezug nötig ist.
fragiles Testdesign// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.DeterministicClockBoundary.javapackage 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());
}
}
DeterministicClockBoundaryTest.javapackage 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))));
}
}
UUID.randomUUID() steckt mitten im Use Case; Assertions kennen nur Formate, nicht die erzeugte Identität.
Generator Port + Deterministic Stub
Exakte Zuordnung zwischen erzeugtem Aggregat, Event und Persistenzschlüssel.
Wert aus dem Request übernehmen, wenn die Identität fachlich vom Aufrufer kommt.
fragiles Testdesign// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.DeterministicIdGenerator.javapackage 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));
}
}
DeterministicIdGeneratorTest.javapackage 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())
);
}
}
Tests greifen auf statische Logger, Mail-Singletons oder globale Listen zu und beeinflussen sich gegenseitig.
Output Port + Recording Fake
Inhalt, Reihenfolge und Empfänger sichtbarer Ausgaben ohne globale Zustände.
Mock verwenden, wenn exakt ein Protokollaufruf selbst Vertragsbestandteil ist.
fragiles Testdesign// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.OutputCapturePort.javapackage 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."));
}
}
OutputCapturePortTest.javapackage 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()
);
}
}
Asynchrone Tests schlafen pauschal mehrere Sekunden und sind langsam oder auf belasteten Systemen instabil.
Polling Probe + Time Budget
Ergebnis innerhalb eines Budgets, aussagekräftige letzte Beobachtung und sofortiger Erfolg.
CompletionStage direkt awaiten, wenn der Produktionsvertrag bereits einen Abschluss-Handle liefert.
fragiles Testdesign// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.EventuallyProbe.javapackage 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); }
}
}
}
EventuallyProbeTest.javapackage 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)));
}
}
Beispieltests prüfen wenige bekannte Werte, während ganze Klassen gültiger Eingaben ungeschützt bleiben.
Property/Invariant Harness + Deterministic Seed
Algebraische Invarianten, Grenzwerte, reproduzierbare Zufallsfolgen und minimale Gegenbeispiele.
Klassische Parameterized Tests, wenn die Eingabemenge klein und vollständig aufzählbar ist.
fragiles Testdesign// Nichtdeterministische Infrastruktur wird direkt aus dem Use Case angesprochen.
// Tests verwenden Sleeps, globale Zustände oder nur schwache Format-Assertions.InvariantTestHarness.javapackage 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);
}
}
}
}
InvariantTestHarnessTest.javapackage 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));
}
}
Fünf Labs stärken den Testvertrag: vollständige Zustandsmatrizen, kanonische Golden Master, mutationsfeste Oracles, stabile Exception-Verträge und reproduzierbare Concurrency-Stresstests.
Einzeltests prüfen nur glückliche Pfade; verbotene Statuswechsel und terminale Zustände bleiben ungeschützt.
Transition Table + Parameterized Contract
Alle erlaubten und verbotenen Übergänge sowie unveränderte Zustände bei Ablehnung.
Property Tests, wenn die Zustandsmaschine sehr groß oder datengetrieben ist.
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.StateTransitionMatrix.javapackage 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);
}
}
StateTransitionMatrixTest.javapackage 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)
);
}
}
Große Legacy-Ausgaben werden mit dutzenden fragilen Feldassertions geprüft oder gar nicht abgesichert.
Golden Master + Canonical Serializer
Stabile fachliche Ausgabe, deterministische Reihenfolge und bewusst sichtbare Regressionen.
Gezielte Assertions, wenn nur wenige fachlich relevante Werte existieren.
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.CanonicalGoldenMaster.javapackage 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;
}
}
CanonicalGoldenMasterTest.javapackage 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));
}
}
Tests prüfen nur not-null, Größe oder Erfolgsstatus; falsche Operatoren und vertauschte Fachwerte überleben unentdeckt.
Behavioral Oracle + Rich Result
Fachentscheidung, Betrag, Begründung und Nebenbedingungen werden gemeinsam präzise geprüft.
Approval Test für sehr große strukturierte Ergebnisse, ergänzt um kritische Invarianten.
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.MutationResistantAssertions.javapackage 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");
}
}
MutationResistantAssertionsTest.javapackage 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())
);
}
}
Tests erwarten nur irgendeine Exception; Fehlercode, Ursache und fachlicher Kontext können unbemerkt verloren gehen.
Exception Translation + Contract Test
Stabiler Fehlercode, relevante Kontextdaten und erhaltene technische Ursache.
Result Type für erwartbare fachliche Ablehnungen ohne Ausnahmefluss.
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.ExceptionContract.javapackage 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);
}
}
}
ExceptionContractTest.javapackage 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())
);
}
}
Ein einziger Thread-Test bestätigt nur den Happy Path; Lost Updates und doppelte Reservierungen bleiben zufällig unsichtbar.
Start Gate + Concurrent Stress Harness
Gleichzeitiger Start, wiederholte Ausführung und harte Invariante über atomare Ergebnisse.
JCStress oder spezialisierte Model Checker für formale Low-Level-Concurrency-Analyse.
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.ConcurrencyStressHarness.javapackage 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(); }
}
ConcurrencyStressHarnessTest.javapackage 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())
);
}
}
Fünf Labs machen Integrationstests belastbar: Ressourcen-Lebenszyklus, Transaktionsisolation, HTTP- und Messaging-Verträge sowie ausführbare Architekturregeln.
Container-Start, Ports und Cleanup werden in jedem Test neu und unterschiedlich verdrahtet.
Resource Fixture + Lifecycle Owner
Ein zentraler Fixture-Owner stellt stabile Verbindungsdaten bereit und garantiert deterministisches Cleanup.
Framework-spezifische Container-Extension, wenn ausschließlich ein Testframework verwendet wird.
schwacher Testvertrag// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.TestcontainersFixtureBoundary.javapackage 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();
}
}
TestcontainersFixtureBoundaryTest.javapackage 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()));
}
}
Integrationstests hinterlassen Daten oder reinigen Tabellen manuell in fehleranfälliger Reihenfolge.
Transaction Script + Test Fixture
Jeder Test läuft in einer kontrollierten Transaktion und beendet sich garantiert mit Rollback.
Schema-per-Test, wenn parallele Tests vollständige Isolation benötigen.
schwacher Testvertrag// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.TransactionRollbackHarness.javapackage 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(); }
}
}
TransactionRollbackHarnessTest.javapackage 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());
}
}
HTTP-Tests mocken interne Clientmethoden und übersehen Pfad, Header, Statuscode oder Payload-Vertrag.
Stub Server + Contract Object
Der Stub zeichnet echte Request-Metadaten auf und antwortet über einen expliziten Vertrag.
Consumer-Driven Contract Framework, wenn mehrere Teams denselben Provider unabhängig entwickeln.
schwacher Testvertrag// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.HttpContractStub.javapackage 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; }
}
HttpContractStubTest.javapackage 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")));
}
}
Messaging-Tests rufen Handler direkt mit beliebigen Strings auf und ignorieren Schlüssel, Version, Header und Duplikate.
Consumer Contract + Envelope
Ein typisierter Envelope macht Versionierung, Korrelation und Idempotenz zum Testvertrag.
Schema Registry Compatibility Tests für organisationsweite Event-Governance.
schwacher Testvertrag// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.MessageConsumerContract.javapackage 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;
}
}
MessageConsumerContractTest.javapackage 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())));
}
}
Architekturregeln stehen nur in Diagrammen; verbotene Abhängigkeiten werden erst im Review entdeckt.
Fitness Function + Dependency Rule
Eine ausführbare Regel prüft Paketabhängigkeiten und liefert präzise Verletzungen.
ArchUnit, wenn Bytecode-basierte Regeln und umfangreiche DSL benötigt werden.
schwacher Testvertrag// Infrastruktur oder Vertrag wird innerhalb einzelner Tests improvisiert.
// Lebenszyklus, Protokoll und Cleanup bleiben verborgen oder inkonsistent.ArchitectureFitnessRule.javapackage 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());
}
}
ArchitectureFitnessRuleTest.javapackage 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()));
}
}
Fünf Labs stärken Testoracles, breite Eingaberäume und zeitlich überlappende Systemverträge - ohne Tests an interne Implementierungsdetails zu koppeln.
Snapshots enthalten Zeitstempel, zufällige IDs oder instabile Reihenfolgen und erzeugen dadurch Rauschen statt Vertrauen.
Approval Testing + Canonicalizer
Vor dem Vergleich werden volatile Felder entfernt und fachlich relevante Reihenfolgen kanonisiert.
Explizite Feldassertions, wenn das Ergebnis klein und die relevanten Felder vollständig bekannt sind.
schwacher Testvertrag// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.SemanticSnapshotApproval.javapackage 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) {}
}
SemanticSnapshotApprovalTest.javapackage 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());
}
}
Beispieltests prüfen nur wenige handverlesene Werte und übersehen Grenzfälle oder unerwartete Kombinationen.
Generator Boundary + Deterministic Seed
Ein reproduzierbarer Generator liefert viele Fälle; bei Fehlern bleibt der Seed als Gegenbeispiel erhalten.
Decision Tables, wenn der Eingaberaum klein, endlich und fachlich vollständig aufzählbar ist.
schwacher Testvertrag// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.PropertyCaseGenerator.javapackage 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);
}
}
PropertyCaseGeneratorTest.javapackage 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));
}
}
Tests mocken interne Methodenketten und brechen bei jeder harmlosen Umstrukturierung, obwohl das fachliche Verhalten gleich bleibt.
Recording Spy + Observable Port
Nur fachlich relevante Interaktionen an einem öffentlichen Port werden aufgezeichnet und gezielt ausgewertet.
State Verification, wenn der resultierende Zustand den Vertrag vollständig beschreibt.
schwacher Testvertrag// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.InteractionSpyBoundary.javapackage 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();
}
}
InteractionSpyBoundaryTest.javapackage 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());
}
}
Migrationen werden nur auf syntaktische Ausführbarkeit geprüft; alte Anwendungsversionen können danach trotzdem nicht mehr lesen oder schreiben.
Compatibility Test + Expand/Contract
Ein maschinenlesbares Schema-Modell prüft, dass bestehende Spalten erhalten und neue Pflichtfelder kompatibel eingeführt werden.
Containerbasierter Vorwärts-/Rückwärts-Test gegen eine echte Datenbank für provider-spezifische Semantik.
schwacher Testvertrag// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.SchemaCompatibilityVerifier.javapackage 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);
}
}
SchemaCompatibilityVerifierTest.javapackage 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());
}
}
Provider-Tests prüfen nur den eigenen Controller, aber nicht die Felder und Bedeutungen, auf die reale Consumer angewiesen sind.
Consumer Contract + Contract Fixture
Der Consumer veröffentlicht einen kleinen stabilen Vertrag; Provider-Antworten werden dagegen unabhängig von internen DTOs geprüft.
Schema-First OpenAPI-Validierung, wenn alle Consumer denselben zentral verwalteten Vertrag akzeptieren.
schwacher Testvertrag// Testdaten, Oracle oder Infrastruktur sind direkt im Test improvisiert.
// Der Test ist entweder instabil oder zu eng an Implementierungsdetails gekoppelt.ConsumerContractFixture.javapackage 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);
}
}
ConsumerContractFixtureTest.javapackage 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());
}
}
Fünf Labs machen Grenzwerte, fehlende Oracles, Fehlerpfade, Ressourcenbesitz und Performance-Budgets als stabile Testverträge ausführbar.
Grenzwerte werden nur zufällig oder durch wenige Happy-Path-Beispiele geprüft; Off-by-one-Fehler bleiben unentdeckt.
Boundary Matrix + Equivalence Partitions
Die Testmatrix erzeugt Werte direkt unter, auf und über jeder relevanten Fachgrenze und ordnet sie erwarteten Partitionen zu.
Property-Based Testing, wenn viele kombinierte Grenzen und große Eingaberäume untersucht werden müssen.
schwacher Testvertrag// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.BoundaryValueMatrix.javapackage 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);
}
}
BoundaryValueMatrixTest.javapackage 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));
}
}
Für komplexe Berechnungen ist kein vollständiges erwartetes Ergebnis bekannt; Tests vergleichen deshalb nur einzelne Beispiele.
Metamorphic Testing + Relation Oracle
Statt exakter Ergebnisse werden stabile Beziehungen geprüft, etwa Monotonie, Skalierung oder Permutationsinvarianz.
Golden Master, wenn ein vertrauenswürdiger Legacy-Ausgangszustand bereits vorhanden ist.
schwacher Testvertrag// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.MetamorphicTestOracle.javapackage 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);
}
}
MetamorphicTestOracleTest.javapackage 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());
}
}
Fehlerpfade werden nur durch schwer reproduzierbare Infrastrukturstörungen erreicht und bleiben dadurch praktisch ungetestet.
Fault Injection + Controlled Failure Port
Ein expliziter Fehlerplan injiziert definierte Ausfälle an stabilen Ports und dokumentiert, welcher Aufruf scheitern soll.
Chaos Engineering in einer produktionsnahen Umgebung, wenn Systemverhalten über mehrere echte Dienste geprüft werden muss.
schwacher Testvertrag// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.FaultInjectionHarness.javapackage 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); }
}
}
FaultInjectionHarnessTest.javapackage 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());
}
}
Tests prüfen nur das fachliche Ergebnis, aber nicht, ob Streams, Sessions oder Handles nach Fehlern zuverlässig geschlossen werden.
Lifecycle Probe + Resource Accounting
Ein zählender Lifecycle-Port macht Öffnen und Schließen sichtbar und prüft nach jeder Ausführung die Ressourceninvariante.
Profiler oder OS-Level-Metriken für native Ressourcen und langfristige Lasttests.
schwacher Testvertrag// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.ResourceLeakProbe.javapackage 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();
}
}
}
}
ResourceLeakProbeTest.javapackage 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());
}
}
Performance wird nur subjektiv bewertet oder erst nach Produktionsproblemen gemessen; Regressionen haben keinen ausführbaren Grenzwert.
Performance Budget + Measurement Boundary
Ein Messport trennt Zeitquelle, Warmup und Budgetentscheidung; Tests prüfen klar definierte Service-Level-Grenzen.
JMH für präzise Mikrobenchmarks oder Lasttests für End-to-End-Durchsatz und Perzentile.
schwacher Testvertrag// Grenzfälle, Fehler oder nichtfunktionale Eigenschaften werden nur implizit geprüft.
// Das Testoracle ist unvollständig, instabil oder nicht reproduzierbar.PerformanceBudget.javapackage 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) {}
}
PerformanceBudgetTest.javapackage 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());
}
}
Fünf Labs entkoppeln Frameworks, asynchrone Beobachtung, strukturierte Logs, Mutationsergebnisse und Flake-Evidenz von instabilen Testdetails.
Fachlogik steckt direkt in Controller-, Listener- oder Framework-Callbacks und ist nur mit schwergewichtiger Laufzeit testbar.
Humble Object + Functional Core
Die extrahierte Entscheidungslogik wird ohne Container, Netzwerk oder UI deterministisch getestet.
Slice- oder Integrationstest, wenn die Framework-Bindung selbst das primäre Risiko ist.
schwacher Testvertrag// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.HumbleObjectBoundary.javapackage 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");
}
}HumbleObjectBoundaryTest.javapackage 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());
}
}Asynchrone Tests greifen auf interne Futures zu oder warten mit festen Sleeps; Reihenfolge und Korrelation bleiben unklar.
Test Probe + Event Recorder
Die Probe zeichnet korrelierte Ereignisse auf und erlaubt eine zeitbudgetierte, semantische Abfrage.
Eventually Probe aus Abschnitt 2, wenn nur ein einzelner Zustand statt einer Ereignisfolge relevant ist.
schwacher Testvertrag// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.AsyncEventProbe.javapackage 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();
}
}AsyncEventProbeTest.javapackage 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());
}
}Tests durchsuchen Konsolentext oder globale Logger-Ausgaben und brechen bei Layout-, Timestamp- oder Thread-Namen-Änderungen.
Output Port + Recording Fake
Fachlich relevante Felder wie Eventtyp, Korrelation und Status werden strukturiert aufgezeichnet und gezielt geprüft.
Tracing-Test-Exporter, wenn verteilte Spans und Parent-Child-Beziehungen der eigentliche Vertrag sind.
schwacher Testvertrag// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.StructuredLogTestSink.javapackage 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));
}
}StructuredLogTestSinkTest.javapackage 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"));
}
}Hohe Coverage suggeriert Sicherheit, obwohl Assertions keine fachlichen Fehlentscheidungen erkennen.
Quality Gate + Mutation Testing
Der Gate bewertet getötete, überlebende und ausgeschlossene Mutanten und liefert eine klare Freigabeentscheidung.
Mutation-Resistant Assertions aus Abschnitt 3, wenn zunächst einzelne schwache Tests gezielt verbessert werden.
schwacher Testvertrag// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.MutationScoreGate.javapackage 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());
}
}MutationScoreGateTest.javapackage 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());
}
}Instabile Tests werden pauschal wiederholt oder quarantänisiert, ohne Häufigkeit, Seed oder erste Abweichung zu dokumentieren.
Repeatable Test Harness + Failure Evidence
Der Harness führt eine Operation begrenzt oft aus, stoppt beim ersten Fehler und liefert reproduzierbare Evidenz.
Concurrency Stress Harness aus Abschnitt 3, wenn die Invariante unter echter paralleler Last geprüft werden muss.
schwacher Testvertrag// Test ist an Laufzeitdetails, globale Zustände oder instabile Ausgaben gekoppelt.
// Fehler liefern keine reproduzierbare fachliche Evidenz.FlakeRepetitionHarness.javapackage 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, "");
}
}FlakeRepetitionHarnessTest.javapackage 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"));
}
}Fünf Labs machen Idempotenz, Serialisierung, Retry-Verhalten, Parallelmigration und zeitliche Kopplung als reproduzierbare Testverträge sichtbar.
Wiederholte Zustellung wird nur im Happy Path geprüft; doppelte Befehle können denselben Seiteneffekt mehrfach auslösen.
Replay Harness + Idempotency Key
Denselben fachlichen Befehl mehrfach abspielen und beweisen, dass genau ein Ergebnis und genau ein Seiteneffekt entsteht.
Message Consumer Contract, wenn primär Envelope, Version und Korrelation statt Seiteneffekt-Deduplizierung geprüft werden.
schwacher Testvertrag// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.IdempotencyReplayHarness.javapackage 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; }
}IdempotencyReplayHarnessTest.javapackage 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());
}
}Serialisierung wird nur per Stringvergleich geprüft; Feldreihenfolge, Escaping und Rücklesbarkeit bleiben ungeschützt.
Round-Trip Contract + Canonical Form
Objekt in eine kanonische Form schreiben, wieder lesen und die fachlich relevanten Werte als stabilen Vertrag prüfen.
Golden Master, wenn eine große bestehende Ausgabe zunächst unverändert abgesichert werden muss.
schwacher Testvertrag// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.SerializationRoundTripContract.javapackage 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", "%"); }
}SerializationRoundTripContractTest.javapackage 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)));
}
}Retry-Tests prüfen nur die Aufrufzahl; Fehlerklassifizierung, Abbruch und Erfolg nach transientem Fehler bleiben unklar.
Policy Object + Scripted Failure
Eine explizite Retry-Policy mit skriptbaren Fehlerfolgen testen und permanente Fehler sicher vom Retry ausschließen.
Fault Injection Harness, wenn ein ganzer Recovery-Workflow mit mehreren Komponenten geprüft werden soll.
schwacher Testvertrag// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.RetryPolicyContract.javapackage 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();
}
}RetryPolicyContractTest.javapackage 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());
}
}Bei Legacy-Migrationen wird die neue Implementierung isoliert getestet; semantische Abweichungen zur Altlogik bleiben unentdeckt.
Differential Testing + Dual Run
Legacy- und Zielimplementierung mit denselben Fällen ausführen, Abweichungen normalisieren und als präzise Migrationsdifferenz ausgeben.
Characterization Test, wenn nur der bestehende Zustand eingefroren und noch keine Zielimplementierung verglichen wird.
schwacher Testvertrag// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.DifferentialTestOracle.javapackage 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);
}
}DifferentialTestOracleTest.javapackage 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());
}
}Tests rufen Methoden in impliziter Reihenfolge auf; bei falscher Reihenfolge entstehen zufällige Exceptions ohne fachliche Erklärung.
Scenario DSL + Explicit State Machine
Erlaubte Schritte als kleine Scenario-DSL und explizite Zustandsmaschine formulieren, sodass Reihenfolgeverträge lesbar und testbar werden.
State Transition Matrix, wenn viele Zustände und Übergänge tabellarisch vollständig geprüft werden müssen.
schwacher Testvertrag// Test kennt Laufzeitdetails oder prüft nur zufällige Darstellung.
// Fehler sind nicht reproduzierbar und liefern keine fachliche Evidenz.TemporalCouplingScenario.javapackage 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);
}
}TemporalCouplingScenarioTest.javapackage 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"));
}
}Fünf Labs machen Zeitsteuerung, Eventual Consistency, Autorisierung, Rückwärtskompatibilität und sichere Testauswahl als reproduzierbare Verträge sichtbar.
Echte Scheduler, Sleeps und Wanduhren machen Tests langsam und flüchtig.
Manual Scheduler + Virtual Time
Zeitgesteuerte Aufgaben ohne echte Wartezeit ausführen und Reihenfolge, Fälligkeit sowie Wiederholungen deterministisch prüfen.
Eventually Probe, wenn nur ein beobachtbares Ergebnis und nicht der Zeitplan selbst relevant ist.
schwacher Testvertrag// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.DeterministicSchedulerHarness.javapackage 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; }
}DeterministicSchedulerHarnessTest.javapackage 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());
}
}Tests hoffen, dass ein Read Model irgendwann aktualisiert ist, und kaschieren Race Conditions mit Sleeps.
Scenario Harness + Explicit Projection Step
Schreib- und Lesemodell mit expliziten Projektionsschritten testen, ohne zufälliges Polling oder versteckte Threads.
Async Event Probe, wenn die reale asynchrone Infrastruktur bewusst Teil des Integrationstests ist.
schwacher Testvertrag// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.EventualConsistencyScenario.javapackage 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(); }
}EventualConsistencyScenarioTest.javapackage 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"));
}
}Sicherheitsregeln werden nur durch einzelne Happy-Path-Tests geprüft; negative Fälle und neue Aktionen bleiben offen.
Decision Table + Deny by Default
Rollen, Aktionen und Ressourcen als vollständige Entscheidungsmatrix testen und fehlende Regeln als Fehler behandeln.
Property-Based Cases, wenn sehr viele Rollen- und Ressourcenattribute kombiniert werden.
schwacher Testvertrag// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.AuthorizationMatrixContract.javapackage 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(); }
}AuthorizationMatrixContractTest.javapackage 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"));
}
}Tests verwenden nur die aktuelle Payload; alte Felder, Umbenennungen und additive Änderungen werden unbemerkt gebrochen.
Compatibility Fixture + Tolerant Reader
Alte und neue Payload-Versionen gegen denselben fachlichen Decoder prüfen und Pflichtwerte explizit absichern.
Consumer-Driven Contract, wenn externe Consumer ihre Erwartungen unabhängig veröffentlichen.
schwacher Testvertrag// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.BackwardCompatibleFixtureContract.javapackage 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;
}
}BackwardCompatibleFixtureContractTest.javapackage 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")));
}
}Testauswahl basiert auf Dateinamen oder Bauchgefühl; wichtige Regressionstests werden bei Querschnittsänderungen ausgelassen.
Dependency Map + Safe Fallback
Geänderte Komponenten auf relevante Test-Suites abbilden und bei unbekannten Änderungen bewusst auf den sicheren Volltest zurückfallen.
Architecture Fitness Rule, wenn nur verbotene Abhängigkeiten und nicht Testauswahl abgesichert werden sollen.
schwacher Testvertrag// Zeit, Infrastruktur oder Sicherheitsentscheidungen bleiben implizit.
// Einzelne Happy Paths liefern keine reproduzierbare fachliche Evidenz.TestImpactSelector.javapackage 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);
}
}TestImpactSelectorTest.javapackage 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")));
}
}Die letzten fünf Labs schließen den Testing-Bereich mit Suite-Portfolio, Parallelisolation, Datenschutz, Recovery und einer evidenzbasierten Release-Entscheidung ab.
Die Suite wächst zufällig: zu viele langsame End-to-End-Tests, doppelte Szenarien und keine sichtbare Verteilung nach Testebene.
Portfolio Test + Layer Budget
Testportfolio nach Unit, Component, Integration und End-to-End klassifizieren und Budgetverletzungen früh sichtbar machen.
Test Impact Selection, wenn die Testebenen bereits sauber sind und nur die Auswahl pro Änderung optimiert werden soll.
schwacher Testvertrag// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.TestSuiteLayerBudget.javapackage 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);
}
}TestSuiteLayerBudgetTest.javapackage 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));
}
}Parallel laufende Tests teilen feste IDs, Tabellenzeilen, Ports oder Verzeichnisse und beeinflussen sich gegenseitig.
Isolated Namespace + Lease
Jeden Testlauf mit einem eindeutigen Namespace und explizitem Lebenszyklus isolieren.
Serialisierung der Tests nur als kurzfristige Übergangslösung, wenn die Infrastruktur noch keine Isolation unterstützt.
schwacher Testvertrag// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.ParallelTestIsolation.javapackage 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); }
}ParallelTestIsolationTest.javapackage 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));
}
}Tests kopieren produktionsnahe personenbezogene Daten, Tokens oder vollständige Payloads in Fixtures und Snapshots.
Synthetic Fixture + Data Minimization
Nur synthetische, minimale und klassifizierte Testdaten zulassen und sensible Felder vor Speicherung ablehnen.
Tokenisierung durch einen externen Testdatendienst, wenn realistische Datenverteilungen zwingend benötigt werden.
schwacher Testvertrag// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.SensitiveTestDataBoundary.javapackage 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);
}
}SensitiveTestDataBoundaryTest.javapackage 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"));
}
}Tests prüfen nur den Normalfall; Crash, Wiederanlauf, doppelte Zustellung und teilweise Persistenz bleiben unbewiesen.
Failure Script + Recovery Oracle
Fehlerpunkte skripten, Neustart simulieren und fachliche Recovery-Invarianten explizit prüfen.
Fault Injection Harness, wenn einzelne technische Fehler statt eines vollständigen Wiederanlaufszenarios untersucht werden.
schwacher Testvertrag// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.RecoveryScenarioHarness.javapackage 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 {}
}RecoveryScenarioHarnessTest.javapackage 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"));
}
}Freigaben beruhen auf einem grünen Gesamtstatus, obwohl kritische Verträge, Mutationsergebnis oder Architekturregeln fehlen.
Quality Gate + Evidence Aggregator
Heterogene Testevidenz gewichten und eine nachvollziehbare Freigabeentscheidung mit konkreten Blockern erzeugen.
Ein einfaches CI-Pass/Fail reicht nur bei kleinen Systemen ohne unterschiedliche Risikoklassen.
schwacher Testvertrag// Testevidenz bleibt implizit, nicht isoliert oder nicht freigaberelevant.
// Ein grüner Einzeltest beweist weder Portfolioqualität noch Recovery oder Datenschutz.ReleaseConfidenceGate.javapackage 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);
}
}ReleaseConfidenceGateTest.javapackage 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());
}
}