Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:34 +08:00
commit 390afca02b
220 changed files with 86013 additions and 0 deletions

View File

@@ -0,0 +1,491 @@
# 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<CompletableFuture<Report>> 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<CompletableFuture<AnalyticsResult>> 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<AnalyticsResult> 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<SearchResults> 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