# Testing Strategies for Sagas ## Unit Testing Saga Logic Test saga behavior with Axon test fixtures: ```java @Test void shouldDispatchPaymentCommandWhenOrderCreated() { // Arrange String orderId = UUID.randomUUID().toString(); String paymentId = UUID.randomUUID().toString(); SagaTestFixture fixture = new SagaTestFixture<>(OrderSaga.class); // Act & Assert fixture .givenNoPriorActivity() .whenPublishingA(new OrderCreatedEvent(orderId, BigDecimal.TEN, "item-1")) .expectDispatchedCommands(new ProcessPaymentCommand(paymentId, orderId, BigDecimal.TEN)); } @Test void shouldCompensateWhenPaymentFails() { String orderId = UUID.randomUUID().toString(); String paymentId = UUID.randomUUID().toString(); SagaTestFixture fixture = new SagaTestFixture<>(OrderSaga.class); fixture .givenNoPriorActivity() .whenPublishingA(new OrderCreatedEvent(orderId, BigDecimal.TEN, "item-1")) .whenPublishingA(new PaymentFailedEvent(paymentId, orderId, "item-1", "Insufficient funds")) .expectDispatchedCommands(new CancelOrderCommand(orderId)) .expectScheduledEventOfType(OrderSaga.class, null); } ``` ## Testing Event Publishing Verify events are published correctly: ```java @SpringBootTest @WebMvcTest class OrderServiceTest { @MockBean private EventPublisher eventPublisher; @InjectMocks private OrderService orderService; @Test void shouldPublishOrderCreatedEvent() { // Arrange CreateOrderRequest request = new CreateOrderRequest("cust-1", BigDecimal.TEN); // Act String orderId = orderService.createOrder(request); // Assert verify(eventPublisher).publish( argThat(event -> event instanceof OrderCreatedEvent && ((OrderCreatedEvent) event).orderId().equals(orderId)) ); } } ``` ## Integration Testing with Testcontainers Test complete saga flow with real services: ```java @SpringBootTest @Testcontainers class SagaIntegrationTest { @Container static KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka:7.4.0") ); @Container static PostgreSQLContainer postgres = new PostgreSQLContainer<>( "postgres:15-alpine" ); @DynamicPropertySource static void overrideProperties(DynamicPropertyRegistry registry) { registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Test void shouldCompleteOrderSagaSuccessfully(@Autowired OrderService orderService, @Autowired OrderRepository orderRepository, @Autowired EventPublisher eventPublisher) { // Arrange CreateOrderRequest request = new CreateOrderRequest("cust-1", BigDecimal.TEN); // Act String orderId = orderService.createOrder(request); // Wait for async processing Thread.sleep(2000); // Assert Order order = orderRepository.findById(orderId).orElseThrow(); assertThat(order.getStatus()).isEqualTo(OrderStatus.COMPLETED); } } ``` ## Testing Idempotency Verify operations produce same results on retry: ```java @Test void compensationShouldBeIdempotent() { // Arrange String paymentId = "payment-123"; Payment payment = new Payment(paymentId, "order-1", BigDecimal.TEN); paymentRepository.save(payment); // Act - First compensation paymentService.cancelPayment(paymentId); Payment firstResult = paymentRepository.findById(paymentId).orElseThrow(); // Act - Second compensation (should be idempotent) paymentService.cancelPayment(paymentId); Payment secondResult = paymentRepository.findById(paymentId).orElseThrow(); // Assert assertThat(firstResult).isEqualTo(secondResult); assertThat(secondResult.getStatus()).isEqualTo(PaymentStatus.CANCELLED); assertThat(secondResult.getVersion()).isEqualTo(firstResult.getVersion()); } ``` ## Testing Concurrent Sagas Verify saga isolation under concurrent execution: ```java @Test void shouldHandleConcurrentSagaExecutions() throws InterruptedException { // Arrange int numThreads = 10; ExecutorService executor = Executors.newFixedThreadPool(numThreads); CountDownLatch latch = new CountDownLatch(numThreads); // Act for (int i = 0; i < numThreads; i++) { final int index = i; executor.submit(() -> { try { CreateOrderRequest request = new CreateOrderRequest( "cust-" + index, BigDecimal.TEN.multiply(BigDecimal.valueOf(index)) ); orderService.createOrder(request); } finally { latch.countDown(); } }); } latch.await(10, TimeUnit.SECONDS); // Assert long createdOrders = orderRepository.count(); assertThat(createdOrders).isEqualTo(numThreads); } ``` ## Testing Failure Scenarios Test each failure path and compensation: ```java @Test void shouldCompensateWhenInventoryUnavailable() { // Arrange String orderId = UUID.randomUUID().toString(); inventoryService.setAvailability("item-1", 0); // No inventory // Act String result = orderService.createOrder( new CreateOrderRequest("cust-1", BigDecimal.TEN) ); // Wait for saga completion Thread.sleep(2000); // Assert Order order = orderRepository.findById(orderId).orElseThrow(); assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED); // Verify payment was refunded Payment payment = paymentRepository.findByOrderId(orderId).orElseThrow(); assertThat(payment.getStatus()).isEqualTo(PaymentStatus.REFUNDED); } @Test void shouldHandlePaymentGatewayFailure() { // Arrange paymentGateway.setFailureRate(1.0); // 100% failure // Act String orderId = orderService.createOrder( new CreateOrderRequest("cust-1", BigDecimal.TEN) ); // Wait for saga completion Thread.sleep(2000); // Assert Order order = orderRepository.findById(orderId).orElseThrow(); assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED); } ``` ## Testing State Machine Verify state transitions: ```java @Test void shouldTransitionStatesProperly() { // Arrange String sagaId = UUID.randomUUID().toString(); SagaState sagaState = new SagaState(sagaId, SagaStatus.STARTED); sagaStateRepository.save(sagaState); // Act & Assert assertThat(sagaState.getStatus()).isEqualTo(SagaStatus.STARTED); sagaState.setStatus(SagaStatus.PROCESSING); sagaStateRepository.save(sagaState); assertThat(sagaStateRepository.findById(sagaId).get().getStatus()) .isEqualTo(SagaStatus.PROCESSING); sagaState.setStatus(SagaStatus.COMPLETED); sagaStateRepository.save(sagaState); assertThat(sagaStateRepository.findById(sagaId).get().getStatus()) .isEqualTo(SagaStatus.COMPLETED); } ``` ## Test Data Builders Use builders for cleaner test code: ```java public class OrderRequestBuilder { private String customerId = "cust-default"; private BigDecimal totalAmount = BigDecimal.TEN; private List items = new ArrayList<>(); public OrderRequestBuilder withCustomerId(String customerId) { this.customerId = customerId; return this; } public OrderRequestBuilder withAmount(BigDecimal amount) { this.totalAmount = amount; return this; } public OrderRequestBuilder withItem(String productId, int quantity) { items.add(new OrderItem(productId, "Product", quantity, BigDecimal.TEN)); return this; } public CreateOrderRequest build() { return new CreateOrderRequest(customerId, totalAmount, items); } } @Test void shouldCreateOrderWithCustomization() { CreateOrderRequest request = new OrderRequestBuilder() .withCustomerId("customer-123") .withAmount(BigDecimal.valueOf(50)) .withItem("product-1", 2) .withItem("product-2", 1) .build(); String orderId = orderService.createOrder(request); assertThat(orderId).isNotNull(); } ``` ## Performance Testing Measure saga execution time: ```java @Test void shouldCompleteOrderSagaWithinTimeLimit() { // Arrange CreateOrderRequest request = new CreateOrderRequest("cust-1", BigDecimal.TEN); long maxDurationMs = 5000; // 5 seconds // Act Instant start = Instant.now(); String orderId = orderService.createOrder(request); Instant end = Instant.now(); // Assert long duration = Duration.between(start, end).toMillis(); assertThat(duration).isLessThan(maxDurationMs); } ```