Search code examples
c#unit-testingasp.net-coredotnet-httpclientpolly

retry polly unit testing (xunit and moq)


I have .net core weabpi (see code below). I am using polly retry policy (see policy below). I would like to unit test endpoint (getProducts) and test polly retry

I have found these examples but it is not clear how to unit test endpoint and retry policy?

services
  .AddHttpClient<IProductService, ProductService>()
  .AddPolicyHandler(GetRetryPolicy(3, 2)); 

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy(int retryCount, int breakDuration)
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(retryCount, retryAttempt => TimeSpan.FromSeconds(Math.Pow(breakDuration,
            retryAttempt)));
}

.Net core api:

public interface IProductService
{
    Task<IEnumerable<ProductResponse>> GetProducts(string productType);
}
public class ProductService: IProductService
{ 
    private readonly HttpClient _httpClient;
    
    public ProductService(HttpClient httpClient)
    {
         _httpClient = httpClient;
    }
    
    public async Task<IEnumerable<ProductResponse>> GetProducts(string productType)
    {
         var response = await _httpClient.GetAsync("uri");
         ...
    }
}

Solution

  • Update (02/21/24)

    In case of V8 you can make assertions against the strategies' options. In this Polly version the strategies behavior can be configures via XYZStrategyOptions. And gladly these objects are retrievable after you have created your pipeline.

    Here you can find the related documentation page

    // Retrieve the descriptor.
    ResiliencePipelineDescriptor descriptor = pipeline.GetPipelineDescriptor();
    
    // Check the pipeline's composition with the descriptor.
    Assert.Equal(2, descriptor.Strategies.Count);
    
    // Verify the retry settings.
    var retryOptions = Assert.IsType<RetryStrategyOptions>(descriptor.Strategies[0].Options);
    Assert.Equal(4, retryOptions.MaxRetryAttempts);
    
    // Confirm the timeout settings.
    var timeoutOptions = Assert.IsType<TimeoutStrategyOptions>(descriptor.Strategies[1].Options);
    Assert.Equal(TimeSpan.FromSeconds(1), timeoutOptions.Timeout);
    

    The sad truth is you can't really unit test your retry+endpoint logic and here are my reasoning:

    1. The retry is registered on the top of the HttpClient via the DI (AddPolicyHandler). When you are unit testing then you are not relying on the DI rather on individual components.
      1.1 So, an integration test might be more suitable for this. I've already detailed how can you do that via WireMock.Net: 1, 2. The basic idea is to create a local HTTP server (to mock the downstream system) with a predefined response sequence.
    2. After you have defined your retry policy with the max retry count and time penalties, you can not retrieve them easily. So, from a unit testing perspective it is really hard to make sure that the policy has been defined correctly (like the delay is specified in seconds, not in minutes). I've already created a github issue for this, but unfortunately the development of the V8 got stuck.

    Back to your test case. The correct way to articulate your test scenario with the given-when-then structure should be written like this

    • Given a faulty downstream service which returns 5XX responses
    • When I call GetProducts
    • Then it is performed 4 times (1 initial + 3 retry attempts)

    This is not a unit test. It is more like a component/integration test. Why? Because even though you could create an HttpClient mock but in that case there will be no retry policy there.

    There is a workaround: you could manually decorate the underlying handler with the policy via the PolicyHttpMessageHandler. But that's a bad idea in a unit test, because you basically re-implemented the DI part inside your test. And with that you would test your test arrangement code, not your production code.