Unit Tests bilden das Fundament testbarer Event-Driven Architecture. Anders als bei traditionellen REST-basierten Systemen müssen wir hier asynchrone Kommunikation und Event-Infrastruktur testen, ohne tatsächliche Broker zu starten. Das Ziel: schnelle, isolierte Tests, die das Verhalten unserer Producer und Consumer verifizieren.
Angenommen Ihr OrderService produziert OrderPlaced-Events und Ihr PaymentService konsumiert diese. Wie testen Sie diese Komponenten isoliert, ohne Kafka zu starten? Wie simulieren Sie Event-Empfang ohne echte Nachrichten?
Die Antwort liegt in strategischer Abstraktion und geschicktem Mocking der Event-Infrastruktur.
Ein testbarer Producer trennt Geschäftslogik von der Infrastruktur. Betrachten Sie diese Struktur:
Spring Boot - Testbarer OrderService:
@Service
public class OrderService {
private final EventPublisher eventPublisher;
public OrderService(EventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void placeOrder(CreateOrderRequest request) {
// Geschäftslogik
Order order = new Order(request.getCustomerId(), request.getItems());
order.calculateTotal();
// Event-Erzeugung
OrderPlacedEvent event = OrderPlacedEvent.builder()
.orderId(order.getId())
.customerId(order.getCustomerId())
.totalAmount(order.getTotal())
.build();
eventPublisher.publish("order.placed.v1", event);
}
}Python - Testbarer OrderService:
class OrderService:
def __init__(self, event_publisher: EventPublisher):
self.event_publisher = event_publisher
def place_order(self, request: CreateOrderRequest):
# Geschäftslogik
order = Order(request.customer_id, request.items)
order.calculate_total()
# Event-Erzeugung
event = OrderPlacedEvent(
order_id=order.id,
customer_id=order.customer_id,
total_amount=order.total
)
self.event_publisher.publish("order.placed.v1", event)| Ansatz | Vorteile | Nachteile | Einsatzbereich |
|---|---|---|---|
| Interface-Mock | Vollständige Kontrolle | Erfordert Abstraktion | Unit Tests |
| Test-Implementation | Realitätsnahe Simulation | Mehr Setup-Code | Integrationstests |
| In-Memory-Broker | Echte Infrastruktur | Langsamere Tests | Component Tests |
Spring Boot Test:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private EventPublisher eventPublisher;
@InjectMocks
private OrderService orderService;
@Test
void shouldPublishOrderPlacedEventWhenOrderIsCreated() {
// Given
CreateOrderRequest request = CreateOrderRequest.builder()
.customerId("customer-123")
.items(List.of(new OrderItem("product-1", 2)))
.build();
// When
orderService.placeOrder(request);
// Then
ArgumentCaptor<OrderPlacedEvent> eventCaptor =
ArgumentCaptor.forClass(OrderPlacedEvent.class);
verify(eventPublisher).publish(
eq("order.placed.v1"),
eventCaptor.capture()
);
OrderPlacedEvent publishedEvent = eventCaptor.getValue();
assertThat(publishedEvent.getCustomerId()).isEqualTo("customer-123");
assertThat(publishedEvent.getOrderId()).isNotNull();
}
}Python Test:
class TestOrderService:
def test_should_publish_order_placed_event_when_order_created(self):
# Given
mock_publisher = Mock(spec=EventPublisher)
order_service = OrderService(mock_publisher)
request = CreateOrderRequest(
customer_id="customer-123",
items=[OrderItem("product-1", 2)]
)
# When
order_service.place_order(request)
# Then
mock_publisher.publish.assert_called_once()
topic, event = mock_publisher.publish.call_args[0]
assert topic == "order.placed.v1"
assert event.customer_id == "customer-123"
assert event.order_id is not NoneConsumer benötigen ähnliche Abstraktionen. Der PaymentService sollte Event-Empfang von Verarbeitungslogik trennen:
Spring Boot - Testbarer PaymentService:
@Service
public class PaymentService {
public void handleOrderPlaced(OrderPlacedEvent event) {
// Geschäftslogik ohne Kafka-Details
Payment payment = Payment.builder()
.orderId(event.getOrderId())
.amount(event.getTotalAmount())
.status(PaymentStatus.PENDING)
.build();
processPayment(payment);
}
private void processPayment(Payment payment) {
// Zahlungsverarbeitung
}
}Unit Test für Consumer:
@Test
void shouldCreatePendingPaymentWhenOrderPlacedEventReceived() {
// Given
OrderPlacedEvent event = OrderPlacedEvent.builder()
.orderId("order-123")
.customerId("customer-456")
.totalAmount(BigDecimal.valueOf(99.99))
.build();
// When
paymentService.handleOrderPlaced(event);
// Then
// Verifikation über Repository-Mock oder State-Assertions
}Konsistente Testdaten reduzieren Wartungsaufwand und verbessern Lesbarkeit:
Java Test Data Builder:
public class OrderPlacedEventTestDataBuilder {
private String orderId = "default-order-id";
private String customerId = "default-customer-id";
private BigDecimal totalAmount = BigDecimal.valueOf(100.00);
public static OrderPlacedEventTestDataBuilder anOrderPlacedEvent() {
return new OrderPlacedEventTestDataBuilder();
}
public OrderPlacedEventTestDataBuilder withOrderId(String orderId) {
this.orderId = orderId;
return this;
}
public OrderPlacedEventTestDataBuilder withAmount(BigDecimal amount) {
this.totalAmount = amount;
return this;
}
public OrderPlacedEvent build() {
return OrderPlacedEvent.builder()
.orderId(orderId)
.customerId(customerId)
.totalAmount(totalAmount)
.timestamp(Instant.now())
.build();
}
}Verwendung im Test:
@Test
void shouldRejectHighValueOrders() {
// Given
OrderPlacedEvent highValueEvent = anOrderPlacedEvent()
.withAmount(BigDecimal.valueOf(10000.00))
.build();
// When & Then
assertThatThrownBy(() -> paymentService.handleOrderPlaced(highValueEvent))
.isInstanceOf(PaymentLimitExceededException.class);
}Verschiedene Testszenarien erfordern verschiedene Event-Ausprägungen:
| Testszenario | Event-Charakteristik | Builder-Methode |
|---|---|---|
| Happy Path | Gültige Standardwerte | aValidOrderPlacedEvent() |
| Edge Case | Grenzwerte | anOrderWithMaxAmount() |
| Error Case | Ungültige Daten | anInvalidOrderEvent() |
| Performance | Große Datenmengen | aBulkOrderEvent() |
Echte Unit Tests isolieren die zu testende Einheit von allen Abhängigkeiten:
Spring Boot - Vollständige Isolation:
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock private PaymentRepository paymentRepository;
@Mock private PaymentGateway paymentGateway;
@Mock private EventPublisher eventPublisher;
@InjectMocks private PaymentService paymentService;
@Test
void shouldPublishPaymentProcessedEventAfterSuccessfulPayment() {
// Given
OrderPlacedEvent orderEvent = anOrderPlacedEvent().build();
when(paymentGateway.processPayment(any()))
.thenReturn(PaymentResult.success("transaction-123"));
// When
paymentService.handleOrderPlaced(orderEvent);
// Then
verify(eventPublisher).publish(
eq("payment.processed.v1"),
any(PaymentProcessedEvent.class)
);
}
}Unit Tests können auf zwei Arten verifizieren:
State Verification - “Was ist das Ergebnis?”
@Test
void shouldCreatePaymentRecordWithCorrectAmount() {
// When
paymentService.handleOrderPlaced(orderEvent);
// Then - State Verification
Payment createdPayment = paymentService.findPaymentForOrder("order-123");
assertThat(createdPayment.getAmount()).isEqualTo(BigDecimal.valueOf(99.99));
}Behavior Verification - “Welche Interaktionen fanden statt?”
@Test
void shouldCallPaymentGatewayWithCorrectAmount() {
// When
paymentService.handleOrderPlaced(orderEvent);
// Then - Behavior Verification
ArgumentCaptor<PaymentRequest> captor =
ArgumentCaptor.forClass(PaymentRequest.class);
verify(paymentGateway).processPayment(captor.capture());
assertThat(captor.getValue().getAmount()).isEqualTo(BigDecimal.valueOf(99.99));
}Event-Consumer arbeiten oft asynchron. Tests müssen diese Parallelität handhaben:
Python - Async Consumer Test:
@pytest.mark.asyncio
async def test_async_event_processing():
# Given
mock_publisher = AsyncMock(spec=EventPublisher)
service = AsyncPaymentService(mock_publisher)
event = anOrderPlacedEvent().build()
# When
await service.handle_order_placed(event)
# Then
mock_publisher.publish.assert_called_once()Für jeden Unit Test sollten Sie prüfen:
Die Kunst des Unit Testing in Event-Driven Architecture liegt darin, die Asynchronität zu abstrahieren, ohne die Geschäftslogik zu verfälschen. Mit den richtigen Abstraktionen und Mock-Strategien entstehen schnelle, verlässliche Tests, die Refactoring unterstützen und Regression verhindern.