# Resilience4j Testing Patterns ## Circuit Breaker Testing ### Testing State Transitions ```java @SpringBootTest class CircuitBreakerStateTest { @Autowired private PaymentService paymentService; @MockBean private RestTemplate restTemplate; @Test void shouldTransitionToOpenAfterFailures() { // Simulate repeated failures when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class))) .thenThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); // Trigger failures to exceed threshold for (int i = 0; i < 5; i++) { assertThatThrownBy(() -> paymentService.processPayment(new PaymentRequest())) .isInstanceOf(HttpServerErrorException.class); } // Circuit should be open - fallback executes PaymentResponse response = paymentService.processPayment(new PaymentRequest()); assertThat(response.getStatus()).isEqualTo("PENDING"); } @Test void shouldExecuteFallbackWhenCircuitOpen() { when(restTemplate.postForObject(anyString(), any(), eq(PaymentResponse.class))) .thenThrow(new RuntimeException("Service unavailable")); // Force failures to open circuit for (int i = 0; i < 5; i++) { try { paymentService.processPayment(new PaymentRequest()); } catch (Exception ignored) {} } // Circuit is open, fallback provides response PaymentResponse response = paymentService.processPayment(new PaymentRequest()); assertThat(response.getStatus()).isEqualTo("PENDING"); assertThat(response.getMessage()).contains("temporarily unavailable"); } } ``` ### Testing Circuit States Directly ```java @SpringBootTest class CircuitBreakerDirectStateTest { @Autowired private CircuitBreakerRegistry circuitBreakerRegistry; @Test void shouldManuallyOpenAndCloseCircuit() { CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("paymentService"); assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); // Manually open circuit circuitBreaker.transitionToOpenState(); assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN); // Manually close circuit circuitBreaker.transitionToClosedState(); assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.CLOSED); } } ``` ## Retry Testing ### Testing Retry Attempts ```java @SpringBootTest @AutoConfigureWireMock(port = 0) class RetryTest { @Autowired private OrderService orderService; @Test void shouldRetryOnTransientFailure() { // Setup: First two calls fail, third succeeds stubFor(post("/orders") .inScenario("Retry Scenario") .whenScenarioStateIs(STARTED) .willReturn(serverError()) .willSetStateTo("First Failure")); stubFor(post("/orders") .inScenario("Retry Scenario") .whenScenarioStateIs("First Failure") .willReturn(serverError()) .willSetStateTo("Second Failure")); stubFor(post("/orders") .inScenario("Retry Scenario") .whenScenarioStateIs("Second Failure") .willReturn(ok().withBody(""" {"id":1,"status":"CREATED"} """))); Order order = orderService.createOrder(new OrderRequest()); assertThat(order.getId()).isEqualTo(1L); assertThat(order.getStatus()).isEqualTo("CREATED"); // Verify exactly 3 calls were made verify(exactly(3), postRequestedFor(urlEqualTo("/orders"))); } @Test void shouldThrowExceptionAfterMaxRetries() { stubFor(post("/orders").willReturn(serverError())); assertThatThrownBy(() -> orderService.createOrder(new OrderRequest())) .isInstanceOf(Exception.class); // Verify retry attempts (maxAttempts = 3) verify(atLeast(3), postRequestedFor(urlEqualTo("/orders"))); } } ``` ## Rate Limiter Testing ### Testing Rate Limit Enforcement ```java @SpringBootTest class RateLimiterTest { @Autowired private NotificationService notificationService; @Test void shouldRejectRequestsExceedingRateLimit() { // Configuration: 5 permits per second // First 5 requests should succeed for (int i = 0; i < 5; i++) { notificationService.sendEmail(createEmailRequest(i)); } // 6th request should fail immediately (no timeout) assertThatThrownBy(() -> notificationService.sendEmail(createEmailRequest(6)) ).isInstanceOf(RequestNotPermitted.class); } @Test void shouldAlowRequestsAfterWindowReset() throws InterruptedException { // First batch of requests for (int i = 0; i < 5; i++) { notificationService.sendEmail(createEmailRequest(i)); } // Wait for refresh period (1 second) Thread.sleep(1100); // Should succeed - window has reset notificationService.sendEmail(createEmailRequest(5)); } private EmailRequest createEmailRequest(int id) { return EmailRequest.builder() .to("user" + id + "@example.com") .subject("Test " + id) .build(); } } ``` ## Bulkhead Testing ### Testing Semaphore Bulkhead ```java @SpringBootTest class BulkheadSemaphoreTest { @Autowired private ReportService reportService; @Test void shouldLimitConcurrentCalls() { // Configuration: maxConcurrentCalls = 5 CountDownLatch latch = new CountDownLatch(5); List> futures = new ArrayList<>(); // Submit 5 concurrent calls for (int i = 0; i < 5; i++) { futures.add(CompletableFuture.supplyAsync(() -> { latch.countDown(); return reportService.generateReport(new ReportRequest()); })); } // 6th call should be rejected assertThatThrownBy(() -> reportService.generateReport(new ReportRequest()) ).isInstanceOf(BulkheadFullException.class); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } } ``` ### Testing Thread Pool Bulkhead ```java @SpringBootTest class BulkheadThreadPoolTest { @Autowired private AnalyticsService analyticsService; @Test void shouldUseThreadPoolForAsync() { // Configuration: threadPoolSize = 2, queueCapacity = 100 List> futures = new ArrayList<>(); for (int i = 0; i < 10; i++) { futures.add(analyticsService.runAnalytics(new AnalyticsRequest())); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); for (CompletableFuture future : futures) { assertThat(future.join()).isNotNull(); } } } ``` ## Time Limiter Testing ### Testing Timeout Enforcement ```java @SpringBootTest class TimeLimiterTest { @Autowired private SearchService searchService; @Test void shouldTimeoutExceededOperations() { // Configuration: timeoutDuration = 1s SearchQuery slowQuery = new SearchQuery(); slowQuery.setSimulatedDelay(Duration.ofSeconds(2)); assertThatThrownBy(() -> searchService.search(slowQuery).get() ).hasCauseInstanceOf(TimeoutException.class); } @Test void shouldReturnFallbackOnTimeout() { SearchQuery slowQuery = new SearchQuery(); slowQuery.setSimulatedDelay(Duration.ofSeconds(2)); CompletableFuture result = searchService.search(slowQuery); SearchResults results = result.join(); assertThat(results).isNotNull(); assertThat(results.isTimedOut()).isTrue(); assertThat(results.getMessage()).contains("timed out"); } } ``` ## Fallback Method Signature Validation ### Correct Fallback Signatures ```java @Service public class PaymentService { @CircuitBreaker(name = "payment", fallbackMethod = "paymentFallback") public PaymentResponse processPayment(PaymentRequest request) { // method body } // CORRECT: Matches return type and parameters + Exception private PaymentResponse paymentFallback(PaymentRequest request, Exception ex) { // fallback logic } @Retry(name = "product") public Product getProduct(String productId) { // method body } // CORRECT: Can omit Exception parameter private Product getProductFallback(String productId) { // fallback logic } } ``` ### Common Fallback Signature Errors ```java @CircuitBreaker(name = "service", fallbackMethod = "fallback") public String processData(Long id) { } // WRONG: Missing parameter public String fallback(Exception ex) { } // WRONG: Wrong return type public void fallback(Long id, Exception ex) { } // WRONG: Wrong parameter type public String fallback(String id, Exception ex) { } // CORRECT: public String fallback(Long id, Exception ex) { } ``` ## Integration Testing Configuration ### Test Configuration Profile ```yaml # application-test.yml resilience4j: circuitbreaker: instances: testService: registerHealthIndicator: false slidingWindowSize: 5 minimumNumberOfCalls: 3 failureRateThreshold: 50 waitDurationInOpenState: 100ms retry: instances: testService: maxAttempts: 2 waitDuration: 10ms ratelimiter: instances: testService: limitForPeriod: 10 limitRefreshPeriod: 1s timeoutDuration: 10ms ``` ### Test Helper Methods ```java @TestConfiguration public class ResilienceTestConfig { public static void openCircuitBreaker(CircuitBreaker circuitBreaker) { circuitBreaker.transitionToOpenState(); } public static void closeCircuitBreaker(CircuitBreaker circuitBreaker) { circuitBreaker.transitionToClosedState(); } public static void simulateFailures( CircuitBreaker circuitBreaker, int numberOfFailures) { for (int i = 0; i < numberOfFailures; i++) { try { circuitBreaker.executeSupplier(() -> { throw new RuntimeException("Simulated failure"); }); } catch (Exception ignored) {} } } public static void resetCircuitBreaker(CircuitBreaker circuitBreaker) { circuitBreaker.transitionToClosedState(); } } ``` ## Common Testing Mistakes ### Mistake 1: Not Waiting for Sliding Window ```java // WRONG: Circuit might not open yet for (int i = 0; i < 3; i++) { try { service.call(); } catch (Exception e) {} } assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN); // May fail! // CORRECT: Exceed minimumNumberOfCalls before checking for (int i = 0; i < 5; i++) { // minimumNumberOfCalls = 5 try { service.call(); } catch (Exception e) {} } assertThat(circuit.getState()).isEqualTo(CircuitBreaker.State.OPEN); ``` ### Mistake 2: Incorrect Fallback Method Access ```java // WRONG: Fallback method is private, not accessible by AOP @CircuitBreaker(name = "service", fallbackMethod = "fallback") public String process(String data) { } private String fallback(String data, Exception ex) { } // Private - won't work! // CORRECT: Package-private or protected protected String fallback(String data, Exception ex) { } ``` ### Mistake 3: Not Mocking External Dependencies ```java // WRONG: Circuit breaker might open due to real network calls @SpringBootTest class ServiceTest { @Autowired private ServiceWithCircuitBreaker service; // Missing @MockBean for external service @Test void test() { // Real network calls - unpredictable } } // CORRECT: Mock external dependencies @SpringBootTest class ServiceTest { @Autowired private ServiceWithCircuitBreaker service; @MockBean private ExternalService externalService; @Test void test() { when(externalService.call()).thenThrow(new RuntimeException()); // Predictable failure } } ``` ## Performance Considerations ### Memory Usage in Tests - **COUNT_BASED sliding window**: Stores last N call outcomes in memory - **TIME_BASED sliding window**: May require more memory for high-throughput services - Use smaller `slidingWindowSize` in tests to reduce memory footprint ### Timeout Configuration for Tests ```yaml resilience4j: timelimiter: instances: testService: timeoutDuration: 2s # Longer timeout for slower CI/CD environments circuitbreaker: instances: testService: waitDurationInOpenState: 100ms # Shorter for faster test execution ``` ### Avoiding Test Flakiness - Set deterministic timeouts based on CI/CD environment - Use `@ActiveProfiles("test")` for test-specific configurations - Reset circuit breaker state between tests when needed - Mock external services consistently