I'm looking to unit test a HttpClient
that has a Polly
RetryPolicy
and I'm trying to work out how to control what the HTTP
response will be.
I have used a HttpMessageHandler
on the client and then override the Send Async and this works great but when I add a Polly Retry Policy I have to create an instance of HTTP Client using the IServiceCollection
and cant create a HttpMessageHandler
for the client. I have tried using the .AddHttpMessageHandler()
but this then blocks the Poll Retry Policy and it only fires off once.
This is how I set up my HTTP client in my test
IServiceCollection services = new ServiceCollection();
const string TestClient = "TestClient";
services.AddHttpClient(name: TestClient)
.AddHttpMessageHandler()
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
.AddPolicyHandler(KYA_GroupService.ProductMessage.ProductMessageHandler.GetRetryPolicy());
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(TestClient);
public static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetryAsync: OnRetryAsync);
}
private async static Task OnRetryAsync(DelegateResult<HttpResponseMessage> outcome, TimeSpan timespan, int retryCount, Context context)
{
//Log result
}
This will then fire the request when I call _httpClient.SendAsync(httpRequestMessage)
but it actualy create a Http call to address and I need to intercept this some how and return a controlled response.
I would like to test that the policy is used to retry the request if the request fails and completes when it is a complete response.
The main restriction I have is I can't use Moq on MSTest.
You don't want your HttpClient
to be issuing real HTTP requests as part of a unit test - that would be an integration test. To avoid making real requests you need to provide a custom HttpMessageHandler
. You've stipulated in your post that you don't want to use a mocking framework, so rather than mocking HttpMessageHandler
you could provide a stub.
With heavy influence from this comment on an issue on Polly's GitHub page, I've adjusted your example to call a stubbed HttpMessageHandler
which throws a 500 the first time it's called, and then returns a 200 on subsequent requests.
The test asserts that the retry handler is called, and that when execution steps past the call to HttpClient.SendAsync
the resulting response has a status code of 200:
public class HttpClient_Polly_Test
{
const string TestClient = "TestClient";
private bool _isRetryCalled;
[Fact]
public async Task Given_A_Retry_Policy_Has_Been_Registered_For_A_HttpClient_When_The_HttpRequest_Fails_Then_The_Request_Is_Retried()
{
// Arrange
IServiceCollection services = new ServiceCollection();
_isRetryCalled = false;
services.AddHttpClient(TestClient)
.AddPolicyHandler(GetRetryPolicy())
.AddHttpMessageHandler(() => new StubDelegatingHandler());
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(TestClient);
// Act
var result = await configuredClient.GetAsync("https://www.stackoverflow.com");
// Assert
Assert.True(_isRetryCalled);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
public IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(
6,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetryAsync: OnRetryAsync);
}
private async Task OnRetryAsync(DelegateResult<HttpResponseMessage> outcome, TimeSpan timespan, int retryCount, Context context)
{
//Log result
_isRetryCalled = true;
}
}
public class StubDelegatingHandler : DelegatingHandler
{
private int _count = 0;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
if (_count == 0)
{
_count++;
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK));
}
}