Angenommen Sie beginnen ein neues Feature in Ihrer Event-Driven Architecture. Womit fangen Sie an? Mit dem Code? Mit der Architektur? Oder vielleicht… mit den Tests?
Test-first Development wendet das bewährte TDD-Prinzip auf Event-Driven Architecture an. Aber wie schreibt man Tests für Events, die noch nicht existieren? Wie definiert man das Verhalten asynchroner Systeme, bevor man sie implementiert?
Diese Fragen führen uns zum Kern des testgetriebenen Designs in Event-Driven Systemen: Erst verstehen, was passieren soll, dann implementieren, wie es passiert.
Bevor wir in die Implementierung einsteigen: Welche Geschäftsereignisse wollen wir eigentlich modellieren? Welche Events sollte unser System produzieren und konsumieren?
Denken Sie an einen neuen Feature-Request: “Als Kunde möchte ich benachrichtigt werden, wenn meine Bestellung versendet wurde.”
Welche Events fallen Ihnen spontan ein? Welche Services wären beteiligt? Wie würde der Event-Flow aussehen?
Event Storming im Test:
// Red Phase: Test für noch nicht existierende Events
@Test
void shouldNotifyCustomerWhenOrderShipped() {
// Given - Was sind unsere Annahmen?
String customerId = "customer-123";
String orderId = "order-456";
CustomerNotificationAssertion notification = new CustomerNotificationAssertion(customerId);
// When - Welches Event löst die Benachrichtigung aus?
OrderShippedEvent shippedEvent = OrderShippedEvent.builder()
.orderId(orderId)
.customerId(customerId)
.trackingNumber("TRACK-789")
.estimatedDelivery(LocalDate.now().plusDays(2))
.build();
eventPublisher.publish("order.shipped.v1", shippedEvent);
// Then - Was erwarten wir als Ergebnis?
notification.waitForNotification(Duration.ofSeconds(5))
.assertNotificationSent()
.assertNotificationType(NotificationType.ORDER_SHIPPED)
.assertContainsTrackingNumber("TRACK-789");
}Können Sie bereits erkennen, welche Komponenten wir implementieren müssen? Welche Events noch nicht existieren? Welche Services erstellt werden müssen?
Wie wenden wir den klassischen TDD-Zyklus auf Event-Driven Architecture an?
Red Phase - Test schlägt fehl:
// Compilation Error: OrderShippedEvent existiert noch nicht
// Compilation Error: CustomerNotificationAssertion existiert noch nicht
// Compilation Error: NotificationType.ORDER_SHIPPED existiert noch nichtGreen Phase - Minimale Implementierung:
// Erst die Event-Struktur
public class OrderShippedEvent {
private String orderId;
private String customerId;
private String trackingNumber;
private LocalDate estimatedDelivery;
// Constructor, getters, setters...
}
// Dann der Test-Helper
public class CustomerNotificationAssertion {
private final String customerId;
private final List<Notification> receivedNotifications = new ArrayList<>();
public CustomerNotificationAssertion(String customerId) {
this.customerId = customerId;
}
public CustomerNotificationAssertion waitForNotification(Duration timeout) {
// Polling-Mechanismus für asynchrone Verifikation
return this;
}
public CustomerNotificationAssertion assertNotificationSent() {
assertThat(receivedNotifications).isNotEmpty();
return this;
}
}
// Schließlich der Service
@Service
public class CustomerNotificationService {
@EventListener
public void handleOrderShipped(OrderShippedEvent event) {
// Minimale Implementierung für Green Phase
Notification notification = new Notification(
event.getCustomerId(),
NotificationType.ORDER_SHIPPED,
"Your order has been shipped!"
);
sendNotification(notification);
}
}Welche Aspekte haben wir in der Green Phase noch nicht berücksichtigt? Was würden Sie als nächstes refactoren?
Refactor Phase - Design verbessern:
// Verbessertes Event Design
public class OrderShippedEvent implements CorrelatedEvent {
private String eventId = UUID.randomUUID().toString();
private String correlationId;
private Instant timestamp = Instant.now();
private OrderShippedData data;
@Builder
public static class OrderShippedData {
private String orderId;
private String customerId;
private String trackingNumber;
private LocalDate estimatedDelivery;
private ShippingProvider shippingProvider;
}
}
// Robustere Notification-Implementierung
@Service
public class CustomerNotificationService {
private final NotificationTemplateEngine templateEngine;
private final NotificationChannel notificationChannel;
@EventListener
public void handleOrderShipped(OrderShippedEvent event) {
try {
NotificationTemplate template = templateEngine.getTemplate(NotificationType.ORDER_SHIPPED);
Notification notification = template.createNotification(event.getData());
notificationChannel.send(notification);
// Event für erfolgreiche Benachrichtigung
publishNotificationSentEvent(event, notification);
} catch (Exception e) {
// Event für fehlgeschlagene Benachrichtigung
publishNotificationFailedEvent(event, e);
}
}
}Wie schreiben wir Tests für asynchrone Event-Processing? Welche Herausforderungen entstehen durch die Asynchronität?
Temporal Assertions für Events:
@Test
void shouldProcessEventsInCorrectOrder() throws Exception {
// Given - Definiere erwartete Event-Reihenfolge
EventSequenceAssertion sequenceAssertion = new EventSequenceAssertion()
.expectEvent(PaymentProcessedEvent.class)
.then()
.expectEvent(InventoryReservedEvent.class)
.then()
.expectEvent(OrderShippedEvent.class)
.withinTimeout(Duration.ofMinutes(2));
// When - Trigger initial event
OrderPlacedEvent orderEvent = anOrderPlacedEvent().build();
eventPublisher.publish("order.placed.v1", orderEvent);
// Then - Asynchrone Verifikation
sequenceAssertion.waitForCompletion();
}
public class EventSequenceAssertion {
private final List<Class<?>> expectedSequence = new ArrayList<>();
private final List<Object> receivedEvents = new ArrayList<>();
private Duration timeout = Duration.ofSeconds(30);
public EventSequenceAssertion expectEvent(Class<?> eventType) {
expectedSequence.add(eventType);
return this;
}
public EventSequenceAssertion then() {
return this; // Fluent interface für Lesbarkeit
}
public void waitForCompletion() throws InterruptedException {
await().atMost(timeout).until(() -> {
return receivedEvents.size() >= expectedSequence.size() &&
eventsMatchExpectedSequence();
});
}
}Welche Alternativen sehen Sie zu diesem Ansatz? Wie würden Sie mit Events umgehen, die in unterschiedlicher Reihenfolge ankommen können?
Specification by Example übersetzt Geschäftsanforderungen in ausführbare Tests. Aber wie funktioniert das mit Events?
Betrachten Sie diese Geschäftsregel: “Wenn ein Kunde eine Bestellung mit mehr als 100€ Wert aufgibt, soll er automatisch kostenlosen Premium-Versand erhalten.”
Wie würden Sie diese Regel als Event-basierte Specification formulieren?
Business Rule als Test:
@Test
void shouldUpgradeToFreeShippingForHighValueOrders() {
// Given - Business Context
BigDecimal highValue = BigDecimal.valueOf(150.00);
String customerId = "premium-customer-123";
ShippingUpgradeAssertion shippingAssertion = new ShippingUpgradeAssertion();
// When - Business Event occurs
OrderPlacedEvent highValueOrder = anOrderPlacedEvent()
.withCustomerId(customerId)
.withTotalAmount(highValue)
.withShippingMethod(ShippingMethod.STANDARD)
.build();
eventPublisher.publish("order.placed.v1", highValueOrder);
// Then - Business Rule is applied
shippingAssertion.waitForUpgrade(Duration.ofSeconds(10))
.assertShippingMethodUpgraded(ShippingMethod.PREMIUM)
.assertShippingCostWaived()
.assertCustomerNotified();
}Specification Table Testing:
@ParameterizedTest
@CsvSource({
"50.00, STANDARD, STANDARD, false, Standard shipping for low value",
"99.99, STANDARD, STANDARD, false, No upgrade just below threshold",
"100.00, STANDARD, PREMIUM, true, Upgrade at exact threshold",
"150.00, STANDARD, PREMIUM, true, Upgrade for high value",
"200.00, EXPRESS, EXPRESS, false, No change for already premium shipping"
})
void shouldApplyShippingRulesBasedOnOrderValue(
BigDecimal orderValue,
ShippingMethod originalMethod,
ShippingMethod expectedMethod,
boolean expectUpgrade,
String scenario) {
// Given
ShippingUpgradeAssertion assertion = new ShippingUpgradeAssertion();
// When
OrderPlacedEvent orderEvent = anOrderPlacedEvent()
.withTotalAmount(orderValue)
.withShippingMethod(originalMethod)
.build();
eventPublisher.publish("order.placed.v1", orderEvent);
// Then
if (expectUpgrade) {
assertion.waitForUpgrade(Duration.ofSeconds(5))
.assertShippingMethodUpgraded(expectedMethod);
} else {
assertion.waitForProcessing(Duration.ofSeconds(5))
.assertNoShippingUpgrade();
}
}Welche weiteren Geschäftsregeln fallen Ihnen ein, die sich gut als Event-basierte Specifications testen ließen?
Wie können Tests gleichzeitig als lebende Dokumentation für Event-Flows dienen?
Self-Documenting Event Tests:
@Nested
@DisplayName("Order Processing - Complete Flow")
class OrderProcessingFlowSpecification {
@Test
@DisplayName("When customer places valid order, system should process payment, reserve inventory and ship order")
void completeHappyPathFlow() {
given("a customer with valid payment method")
.and("sufficient inventory is available")
.when("customer places an order")
.then("payment should be processed")
.and("inventory should be reserved")
.and("order should be shipped")
.and("customer should be notified");
}
@Test
@DisplayName("When payment fails, system should cancel inventory reservation and notify customer")
void paymentFailureFlow() {
given("a customer with invalid payment method")
.and("sufficient inventory is available")
.when("customer places an order")
.then("payment should fail")
.and("inventory reservation should be cancelled")
.and("customer should be notified of payment failure")
.and("order should be marked as failed");
}
}Python Specification Style:
class TestOrderProcessingSpecification:
def test_high_value_order_gets_free_shipping(self):
"""
Specification: Orders over €100 receive free premium shipping
Given: Customer places order worth €150
When: Order processing begins
Then: Shipping method is upgraded to premium
And: Shipping cost is waived
And: Customer is notified of the upgrade
"""
# Given
order_value = Decimal('150.00')
shipping_assertion = ShippingUpgradeAssertion()
# When
order_event = OrderPlacedEventBuilder() \
.with_total_amount(order_value) \
.with_shipping_method(ShippingMethod.STANDARD) \
.build()
self.event_publisher.publish("order.placed.v1", order_event)
# Then
shipping_assertion.wait_for_upgrade(timeout=timedelta(seconds=10)) \
.assert_shipping_upgraded(ShippingMethod.PREMIUM) \
.assert_shipping_cost_waived() \
.assert_customer_notified()Wie würden Sie sicherstellen, dass diese Tests auch für fachliche Stakeholder verständlich bleiben?
Welche Fallen haben Sie beim Testen von Event-Driven Systemen bereits erlebt? Welche Probleme sind Ihnen begegnet?
Anti-Pattern: Overly Coupled Event Tests
// SCHLECHT: Test ist an konkrete Infrastruktur gekoppelt
@Test
void badEventTest() {
// Hard-coded Kafka Details
KafkaProducer<String, OrderPlacedEvent> producer = new KafkaProducer<>(
Map.of(
"bootstrap.servers", "localhost:9092",
"key.serializer", "org.apache.kafka.common.serialization.StringSerializer"
)
);
// Direct Kafka interaction in test
producer.send(new ProducerRecord<>("order.placed.v1", orderEvent));
// Poll messages directly
KafkaConsumer<String, PaymentProcessedEvent> consumer = new KafkaConsumer<>(consumerProps);
ConsumerRecords<String, PaymentProcessedEvent> records = consumer.poll(Duration.ofSeconds(5));
// Test ist fragil und schwer zu maintainen
}
// BESSER: Abstrahierte Event-Operationen
@Test
void betterEventTest() {
// When
orderService.placeOrder(orderRequest);
// Then
paymentEventAssertion.waitForPaymentProcessed(Duration.ofSeconds(5))
.assertPaymentAmount(expectedAmount);
}Warum ist der erste Ansatz problematisch? Welche Probleme entstehen bei Änderungen der Infrastruktur?
Anti-Pattern: Non-Deterministic Event Timing
// SCHLECHT: Race Conditions in Tests
@Test
void badTimingTest() {
orderService.placeOrder(orderRequest);
// Magische Sleep-Zeiten
Thread.sleep(3000);
// Annahme über Event-Timing
PaymentProcessedEvent event = paymentEventCollector.getLastEvent();
assertThat(event).isNotNull(); // Kann fehlschlagen bei langsamer Verarbeitung
}
// BESSER: Explizite Synchronisation
@Test
void betterTimingTest() {
PaymentEventAssertion assertion = new PaymentEventAssertion();
orderService.placeOrder(orderRequest);
// Warten auf tatsächliches Event
assertion.waitForPaymentProcessed(Duration.ofSeconds(10))
.assertPaymentSuccessful();
}Anti-Pattern: Giant Event Tests
// SCHLECHT: Ein Test testet zu viel
@Test
void badGiantTest() {
// Test versucht gesamten E-Commerce Flow zu testen
// 200+ Zeilen Code
// Multiple Business Rules
// Verschiedene Error Cases
// Schwer zu debuggen bei Fehlschlag
}
// BESSER: Focused Event Tests
@Test
void testPaymentProcessing() {
// Fokus nur auf Payment-Verarbeitung
}
@Test
void testInventoryReservation() {
// Fokus nur auf Inventory-Logik
}
@Test
void testShippingUpgrade() {
// Fokus nur auf Shipping-Rules
}Welche weiteren Anti-Patterns sind Ihnen in Event-Driven Tests begegnet?
Anti-Pattern: Event Data Mutation in Tests
// SCHLECHT: Test verändert Event-Daten
@Test
void badMutationTest() {
OrderPlacedEvent event = getTestEvent();
event.setOrderId("modified-id"); // Test verändert Event-State
event.getData().setTotalAmount(BigDecimal.valueOf(999.99));
// Andere Tests könnten beeinflusst werden
processEvent(event);
}
// BESSER: Immutable Test Events
@Test
void betterImmutableTest() {
OrderPlacedEvent event = anOrderPlacedEvent()
.withOrderId("test-specific-id")
.withTotalAmount(BigDecimal.valueOf(123.45))
.build(); // Jeder Test bekommt frische Event-Instanz
processEvent(event);
}Basierend auf den Anti-Patterns: Welche Prinzipien würden Sie für gute Event-Tests ableiten?
Event Test Design Principles:
| Prinzip | Warum wichtig? | Implementierung |
|---|---|---|
| Event Isolation | Tests beeinflussen sich nicht | Fresh Event instances per test |
| Temporal Decoupling | Keine Race Conditions | Explicit waiting mechanisms |
| Infrastructure Abstraction | Portabilität und Wartbarkeit | Event Publisher/Consumer abstractions |
| Single Responsibility | Debuggability | One business rule per test |
| Deterministic Assertions | Verlässliche Tests | No timing assumptions |
Template für robuste Event Tests:
@Test
void shouldFollowEventTestTemplate() {
// Arrange - Setup test state
String correlationId = "test-correlation-" + UUID.randomUUID();
EventAssertion eventAssertion = new EventAssertion(correlationId);
TestEvent triggerEvent = aTestEvent()
.withCorrelationId(correlationId)
.withSpecificTestData()
.build();
// Act - Trigger event processing
eventPublisher.publish("test.topic.v1", triggerEvent);
// Assert - Verify expected outcomes
eventAssertion.waitForCompletion(Duration.ofSeconds(10))
.assertExpectedEventsReceived()
.assertCorrectEventOrder()
.assertBusinessRulesApplied()
.assertNoUnexpectedSideEffects();
}Welche dieser Prinzipien wären in Ihrem aktuellen Projekt am wichtigsten? Wo sehen Sie die größten Verbesserungsmöglichkeiten?
Test-first Development in Event-Driven Architecture verlangt ein Umdenken: Von synchronen Assertions zu asynchronen Erwartungen, von direkter Kontrolle zu eventbasierter Koordination. Der Schlüssel liegt darin, die Geschäftslogik durch Events zu spezifizieren, bevor man sie implementiert - und dabei die besonderen Herausforderungen asynchroner Systeme von Anfang an mitzudenken.
Welchen Aspekt von TDD mit Events würden Sie als nächstes in Ihrem Team einführen? Mit welchem konkreten Test würden Sie beginnen?