The answer above was very helpful in getting me on the right track. However I wanted to test that Policies had been added to a typed http client. This client is defined at application startup. So the challenge was how to add a stub delegating handler after the handlers specified in the typed client definition and it had been added to the Services collection.
I was able to leverage IHttpMessageHandlerBuilderFilter.Configure and added my stub handler as the last handler in the chain.
public sealed class HttpClientInterceptionFilter : IHttpMessageHandlerBuilderFilter
{
HandlerConfig handlerconfig { get; set; }
public HttpClientInterceptionFilter(HandlerConfig calls)
{
handlerconfig = calls;
}
/// <inheritdoc/>
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
return (builder) =>
{
// Run any actions the application has configured for itself
next(builder);
// Add the interceptor as the last message handler
builder.AdditionalHandlers.Add(new StubDelegatingHandler(handlerconfig));
};
}
}
Register this class with the DI container in your unit test:
services.AddTransient<IHttpMessageHandlerBuilderFilter>(n => new HttpClientInterceptionFilter(handlerConfig));
I needed pass in some parameters to the stub handler and to get data out of it and back to my unit test. I used this class to do so:
public class HandlerConfig
{
public int CallCount { get; set; }
public DateTime[] CallTimes { get; set; }
public int BackOffSeconds { get; set; }
public ErrorTypeEnum ErrorType { get; set; }
}
public enum ErrorTypeEnum
{
Transient,
TooManyRequests
}
My stub handler generates transient and too many request responses:
public class StubDelegatingHandler : DelegatingHandler
{
private HandlerConfig _config;
HttpStatusCode[] TransientErrors = new HttpStatusCode[] { HttpStatusCode.RequestTimeout, HttpStatusCode.InternalServerError, HttpStatusCode.OK };
public StubDelegatingHandler(HandlerConfig config)
{
_config = config;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
_config.CallTimes[_config.CallCount] = DateTime.Now;
if (_config.ErrorType == ErrorTypeEnum.Transient)
{
var response = new HttpResponseMessage(TransientErrors[_config.CallCount]);
_config.CallCount++;
return Task.FromResult(response);
}
HttpResponseMessage response429;
if (_config.CallCount < 2)
{
//generate 429 errors
response429 = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
response429.Headers.Date = DateTime.UtcNow;
DateTimeOffset dateTimeOffSet = DateTimeOffset.UtcNow.Add(new TimeSpan(0, 0, 5));
long resetDateTime = dateTimeOffSet.ToUnixTimeSeconds();
response429.Headers.Add("x-rate-limit-reset", resetDateTime.ToString());
}
else
{
response429 = new HttpResponseMessage(HttpStatusCode.OK);
}
_config.CallCount++;
return Task.FromResult(response429);
}
}
And finally the unit test:
[TestMethod]
public async Task Given_A_429_Retry_Policy_Has_Been_Registered_For_A_HttpClient_When_429_Errors_Occur_Then_The_Request_Is_Retried()
{
// Arrange
IServiceCollection services = new ServiceCollection();
var handlerConfig = new HandlerConfig { ErrorType = ErrorTypeEnum.TooManyRequests, BackOffSeconds = 5, CallTimes = new System.DateTime[RetryCount] };
// this registers a stub message handler that returns the desired error codes
services.AddTransient<IHttpMessageHandlerBuilderFilter>(n => new HttpClientInterceptionFilter(handlerConfig));
services.ConfigureAPIClient(); //this is an extension method that adds a typed client to the services collection
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient("APIClient"); //Note this must be the same name used in ConfigureAPIClient
// Act
var result = await configuredClient.GetAsync("https://localhost/test");
// Assert
Assert.AreEqual(3, handlerConfig.CallCount, "Expected number of calls made");
Assert.AreEqual(HttpStatusCode.OK, result.StatusCode, "Verfiy status code");
var actualWaitTime = handlerConfig.CallTimes[1] - handlerConfig.CallTimes[0];
var expectedWaitTime = handlerConfig.BackOffSeconds + 1; //ConfigureAPIClient adds one second to give a little buffer
Assert.AreEqual(expectedWaitTime, actualWaitTime.Seconds);
}
}