# Resilience Test Rules / Quy Tắc Resilience Testing Hướng dẫn chi tiết cho việc kiểm thử khả năng phục hồi với Polly policies trong .NET Microservices. ## 1. Overview / Tổng Quan Resilience testing đảm bảo hệ thống hoạt động ổn định khi gặp: - **Transient failures**: Lỗi tạm thời (network timeout, 503 errors) - **Partial outages**: Một phần hệ thống ngừng hoạt động - **Cascading failures**: Lỗi lan truyền giữa các services ### Polly Policies Cần Test | Policy | Mục đích | Scenario | |--------|----------|----------| | **Retry** | Thử lại khi gặp lỗi tạm thời | 500, 503, timeout | | **Circuit Breaker** | Ngắt mạch khi lỗi liên tục | Nhiều request thất bại | | **Timeout** | Giới hạn thời gian chờ | Slow responses | | **Bulkhead** | Cô lập resources | Concurrent request limit | | **Fallback** | Giá trị dự phòng | Graceful degradation | ## 2. Mock HTTP Handler / Handler HTTP Giả Lập ### 2.1 Basic Mock Handler ```csharp /// /// EN: Mock HTTP handler for injecting specific responses. /// VI: Mock HTTP handler để inject responses cụ thể. /// public class MockHttpMessageHandler : HttpMessageHandler { private readonly Func _handler; public List ReceivedRequests { get; } = new(); public MockHttpMessageHandler( Func handler) { _handler = handler; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { ReceivedRequests.Add(request); return Task.FromResult(_handler(request, cancellationToken)); } } ``` ### 2.2 Configurable Mock Handler ```csharp /// /// EN: Handler that returns different responses based on call count. /// VI: Handler trả về responses khác nhau dựa trên số lần gọi. /// public class SequentialResponseHandler : HttpMessageHandler { private readonly Queue _responses; private int _callCount = 0; public int CallCount => _callCount; public SequentialResponseHandler(params HttpResponseMessage[] responses) { _responses = new Queue(responses); } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Interlocked.Increment(ref _callCount); if (_responses.Count > 0) return Task.FromResult(_responses.Dequeue()); // Default: return 200 OK when queue is empty return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); } } ``` ### 2.3 Exception Throwing Handler ```csharp /// /// EN: Handler that throws exceptions for testing error handling. /// VI: Handler throw exceptions để test error handling. /// public class ExceptionThrowingHandler : HttpMessageHandler { private readonly Func _exceptionFactory; private int _callCount = 0; public int CallCount => _callCount; public ExceptionThrowingHandler(Func exceptionFactory) { _exceptionFactory = exceptionFactory; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { _callCount++; var exception = _exceptionFactory(_callCount); if (exception != null) throw exception; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)); } } // Usage: Throw exception on first 2 calls, succeed on 3rd var handler = new ExceptionThrowingHandler(callCount => callCount <= 2 ? new HttpRequestException("Network error") : null); ``` ## 3. Retry Policy Testing / Test Retry Policy ### 3.1 Basic Retry Test ```csharp /// /// EN: Tests for retry policy behavior. /// VI: Kiểm thử hành vi của retry policy. /// public class RetryPolicyTests { [Fact] public async Task RetryPolicy_TransientFailure_RetriesAndSucceeds() { // Arrange var handler = new SequentialResponseHandler( new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.OK) ); var retryPolicy = Policy .Handle() .OrResult(r => (int)r.StatusCode >= 500) .WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(10)); var httpClient = new HttpClient(handler); // Act var response = await retryPolicy.ExecuteAsync(() => httpClient.GetAsync("http://test-api/orders")); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); handler.CallCount.Should().Be(3); // 2 retries + 1 success } [Fact] public async Task RetryPolicy_AllRetriesFail_ThrowsException() { // Arrange var handler = new SequentialResponseHandler( new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable) ); var retryPolicy = Policy .Handle() .OrResult(r => (int)r.StatusCode >= 500) .WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(10)); var httpClient = new HttpClient(handler); // Act var response = await retryPolicy.ExecuteAsync(() => httpClient.GetAsync("http://test-api/orders")); // Assert - After all retries, returns last failed response response.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); handler.CallCount.Should().Be(4); // 1 initial + 3 retries } } ``` ### 3.2 Testing Retry with Jitter ```csharp /// /// EN: Tests for retry policy with exponential backoff and jitter. /// VI: Kiểm thử retry policy với exponential backoff và jitter. /// public class RetryWithJitterTests { [Fact] public async Task RetryPolicy_WithJitter_DelaysIncreaseExponentially() { // Arrange var delays = new List(); var retryPolicy = Policy .Handle() .OrResult(r => (int)r.StatusCode >= 500) .WaitAndRetryAsync( retryCount: 3, sleepDurationProvider: (attempt, context) => { var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)) + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100)); delays.Add(delay); return delay; }); var handler = new SequentialResponseHandler( new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.ServiceUnavailable), new HttpResponseMessage(HttpStatusCode.OK) ); var httpClient = new HttpClient(handler); // Act await retryPolicy.ExecuteAsync(() => httpClient.GetAsync("http://test-api/orders")); // Assert delays.Should().HaveCount(3); delays[1].Should().BeGreaterThan(delays[0]); // Exponential increase delays[2].Should().BeGreaterThan(delays[1]); } } ``` ## 4. Circuit Breaker Testing / Test Circuit Breaker ### 4.1 Basic Circuit Breaker Test ```csharp /// /// EN: Tests for circuit breaker policy behavior. /// VI: Kiểm thử hành vi của circuit breaker policy. /// public class CircuitBreakerPolicyTests { [Fact] public async Task CircuitBreaker_ConsecutiveFailures_OpensCircuit() { // Arrange var callCount = 0; var circuitBreaker = Policy .Handle() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 2, durationOfBreak: TimeSpan.FromSeconds(30)); var failingAction = new Func(() => { callCount++; throw new HttpRequestException("Service unavailable"); }); // Act - Trigger failures to open circuit await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(failingAction)); await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(failingAction)); // Assert - Circuit should now be open circuitBreaker.CircuitState.Should().Be(CircuitState.Open); callCount.Should().Be(2); // Further calls should fail immediately with BrokenCircuitException await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(failingAction)); callCount.Should().Be(2); // No additional calls made } [Fact] public async Task CircuitBreaker_Success_ResetsFailureCount() { // Arrange var callCount = 0; var shouldFail = true; var circuitBreaker = Policy .Handle() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 3, durationOfBreak: TimeSpan.FromSeconds(30)); var action = new Func(() => { callCount++; if (shouldFail) throw new HttpRequestException("Failed"); return Task.CompletedTask; }); // Act // First failure await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(action)); // Successful call shouldFail = false; await circuitBreaker.ExecuteAsync(action); // Another failure - should not open circuit (counter reset) shouldFail = true; await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(action)); // Assert circuitBreaker.CircuitState.Should().Be(CircuitState.Closed); } } ``` ### 4.2 Testing Circuit Half-Open State ```csharp /// /// EN: Tests for circuit breaker half-open state behavior. /// VI: Kiểm thử hành vi trạng thái half-open của circuit breaker. /// public class CircuitBreakerHalfOpenTests { [Fact] public async Task CircuitBreaker_AfterBreakDuration_TransitionsToHalfOpen() { // Arrange var breakDuration = TimeSpan.FromMilliseconds(100); var circuitBreaker = Policy .Handle() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 1, durationOfBreak: breakDuration); // Act - Open the circuit await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(() => throw new HttpRequestException("Fail"))); circuitBreaker.CircuitState.Should().Be(CircuitState.Open); // Wait for break duration await Task.Delay(breakDuration + TimeSpan.FromMilliseconds(50)); // Assert - Circuit should now be Half-Open circuitBreaker.CircuitState.Should().Be(CircuitState.HalfOpen); } [Fact] public async Task CircuitBreaker_HalfOpenSuccess_ClosesCircuit() { // Arrange var breakDuration = TimeSpan.FromMilliseconds(100); var shouldFail = true; var circuitBreaker = Policy .Handle() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 1, durationOfBreak: breakDuration); var action = new Func(() => { if (shouldFail) throw new HttpRequestException("Fail"); return Task.CompletedTask; }); // Open the circuit await Assert.ThrowsAsync(() => circuitBreaker.ExecuteAsync(action)); // Wait for half-open await Task.Delay(breakDuration + TimeSpan.FromMilliseconds(50)); // Act - Success in half-open state shouldFail = false; await circuitBreaker.ExecuteAsync(action); // Assert circuitBreaker.CircuitState.Should().Be(CircuitState.Closed); } } ``` ## 5. Timeout Policy Testing / Test Timeout Policy ### 5.1 Basic Timeout Test ```csharp /// /// EN: Tests for timeout policy behavior. /// VI: Kiểm thử hành vi của timeout policy. /// public class TimeoutPolicyTests { [Fact] public async Task TimeoutPolicy_SlowOperation_ThrowsTimeoutRejectedException() { // Arrange var timeoutPolicy = Policy .TimeoutAsync(TimeSpan.FromMilliseconds(100)); var slowOperation = new Func>(async ct => { await Task.Delay(TimeSpan.FromSeconds(5), ct); return new HttpResponseMessage(HttpStatusCode.OK); }); // Act & Assert await Assert.ThrowsAsync(() => timeoutPolicy.ExecuteAsync(slowOperation, CancellationToken.None)); } [Fact] public async Task TimeoutPolicy_FastOperation_Succeeds() { // Arrange var timeoutPolicy = Policy .TimeoutAsync(TimeSpan.FromSeconds(5)); var fastOperation = new Func>(async ct => { await Task.Delay(TimeSpan.FromMilliseconds(50), ct); return new HttpResponseMessage(HttpStatusCode.OK); }); // Act var response = await timeoutPolicy.ExecuteAsync(fastOperation, CancellationToken.None); // Assert response.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] public async Task TimeoutPolicy_OptimisticTimeout_CancelsToken() { // Arrange var tokenWasCancelled = false; var timeoutPolicy = Policy .TimeoutAsync(TimeSpan.FromMilliseconds(100), TimeoutStrategy.Optimistic); var operation = new Func(async ct => { try { await Task.Delay(TimeSpan.FromSeconds(5), ct); } catch (OperationCanceledException) { tokenWasCancelled = true; throw; } }); // Act try { await timeoutPolicy.ExecuteAsync(operation, CancellationToken.None); } catch (TimeoutRejectedException) { // Expected } // Assert tokenWasCancelled.Should().BeTrue(); } } ``` ## 6. Fallback Policy Testing / Test Fallback Policy ### 6.1 Basic Fallback Test ```csharp /// /// EN: Tests for fallback policy behavior. /// VI: Kiểm thử hành vi của fallback policy. /// public class FallbackPolicyTests { [Fact] public async Task FallbackPolicy_OnException_ReturnsFallbackValue() { // Arrange var fallbackOrder = new OrderDto { Id = Guid.Empty, Status = "Fallback" }; var fallbackPolicy = Policy .Handle() .FallbackAsync(fallbackOrder); var failingOperation = new Func>(() => throw new HttpRequestException("Service unavailable")); // Act var result = await fallbackPolicy.ExecuteAsync(failingOperation); // Assert result.Should().BeEquivalentTo(fallbackOrder); } [Fact] public async Task FallbackPolicy_WithFallbackAction_ExecutesAction() { // Arrange var fallbackWasCalled = false; var fallbackPolicy = Policy .Handle() .FallbackAsync( fallbackAction: async (ctx, ct) => { fallbackWasCalled = true; return await Task.FromResult(new OrderDto { Status = "Cached" }); }); // Act var result = await fallbackPolicy.ExecuteAsync(() => throw new HttpRequestException("Failed")); // Assert fallbackWasCalled.Should().BeTrue(); result!.Status.Should().Be("Cached"); } } ``` ## 7. Combined Policies Testing / Test Policies Kết Hợp ### 7.1 PolicyWrap Testing ```csharp /// /// EN: Tests for combined policies (PolicyWrap). /// VI: Kiểm thử các policies kết hợp (PolicyWrap). /// public class PolicyWrapTests { [Fact] public async Task PolicyWrap_CombinedPolicies_AppliesInCorrectOrder() { // Arrange var callCount = 0; var executionLog = new List(); // Outer policy: Circuit Breaker var circuitBreaker = Policy .Handle() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: 5, durationOfBreak: TimeSpan.FromSeconds(30), onBreak: (_, _) => executionLog.Add("CircuitBreak"), onReset: () => executionLog.Add("CircuitReset")); // Inner policy: Retry var retry = Policy .Handle() .RetryAsync(3, onRetry: (_, retryCount) => { executionLog.Add($"Retry-{retryCount}"); }); // Wrap: Retry inside CircuitBreaker var policyWrap = Policy.WrapAsync(circuitBreaker, retry); var handler = new SequentialResponseHandler( CreateFailure(), CreateFailure(), CreateFailure(), new HttpResponseMessage(HttpStatusCode.OK) ); var httpClient = new HttpClient(handler); var operation = new Func>(async () => { callCount++; var response = await httpClient.GetAsync("http://test/api"); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"Status: {response.StatusCode}"); return response; }); // Act var result = await policyWrap.ExecuteAsync(operation); // Assert result.StatusCode.Should().Be(HttpStatusCode.OK); executionLog.Should().Contain("Retry-1"); executionLog.Should().Contain("Retry-2"); executionLog.Should().Contain("Retry-3"); } private static HttpResponseMessage CreateFailure() => new(HttpStatusCode.ServiceUnavailable); } ``` ## 8. Best Practices / Thực Hành Tốt ### Test Time Sensitivity ```csharp /// /// EN: Use short timeouts in tests to avoid slow test suites. /// VI: Dùng timeout ngắn trong tests để tránh test suite chậm. /// public class TimeoutBestPracticeTests { // ❌ BAD: Long timeouts slow down tests private static readonly TimeSpan BadTimeout = TimeSpan.FromSeconds(30); // ✅ GOOD: Short timeouts for fast feedback private static readonly TimeSpan GoodTimeout = TimeSpan.FromMilliseconds(100); } ``` ### Policy Factory for Testing ```csharp /// /// EN: Factory to create policies with testable parameters. /// VI: Factory tạo policies với parameters có thể test. /// public class ResiliencePolicyFactory { private readonly ILogger _logger; public ResiliencePolicyFactory(ILogger logger) { _logger = logger; } public IAsyncPolicy CreateRetryPolicy( int retryCount = 3, TimeSpan? delay = null) { return Policy .Handle() .OrResult(r => (int)r.StatusCode >= 500) .WaitAndRetryAsync( retryCount, attempt => delay ?? TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)), onRetry: (outcome, timespan, retryAttempt, context) => { _logger.LogWarning( "Retry {RetryAttempt} after {Delay}ms due to {Reason}", retryAttempt, timespan.TotalMilliseconds, outcome.Exception?.Message ?? outcome.Result.StatusCode.ToString()); }); } } ``` ### Verifying Retry Delays ```csharp /// /// EN: Custom policy with observable delays for testing. /// VI: Custom policy với delays có thể quan sát để test. /// public class ObservableRetryPolicy { public List ObservedDelays { get; } = new(); public IAsyncPolicy CreatePolicy(int retryCount) { return Policy .Handle() .OrResult(r => !r.IsSuccessStatusCode) .WaitAndRetryAsync( retryCount, sleepDurationProvider: attempt => { var delay = TimeSpan.FromMilliseconds(100 * attempt); ObservedDelays.Add(delay); return delay; }); } } ``` ## 9. Common Pitfalls / Lỗi Thường Gặp ### ❌ Testing Against Real Services ```csharp // ❌ BAD: Flaky tests due to real service availability var client = new HttpClient { BaseAddress = new Uri("https://real-api.com") }; await policy.ExecuteAsync(() => client.GetAsync("/orders")); // ✅ GOOD: Mock handler for deterministic behavior var mockHandler = new MockHttpMessageHandler(...) var client = new HttpClient(mockHandler); await policy.ExecuteAsync(() => client.GetAsync("/orders")); ``` ### ❌ Not Testing Circuit State Transitions ```csharp // ❌ BAD: Only test open state circuitBreaker.CircuitState.Should().Be(CircuitState.Open); // ✅ GOOD: Test all state transitions circuitBreaker.CircuitState.Should().Be(CircuitState.Closed); // ... trigger failures circuitBreaker.CircuitState.Should().Be(CircuitState.Open); // ... wait for break duration circuitBreaker.CircuitState.Should().Be(CircuitState.HalfOpen); // ... success circuitBreaker.CircuitState.Should().Be(CircuitState.Closed); ``` ### ❌ Ignoring Policy Order in Wrap ```csharp // ❌ BAD: Wrong order - retry outside circuit breaker var wrong = Policy.WrapAsync(retry, circuitBreaker); // ✅ GOOD: Retry should be inside circuit breaker // Circuit breaker tracks failures INCLUDING retried calls var correct = Policy.WrapAsync(circuitBreaker, retry); ```