Reputation: 8009
I want to find out how Polly retry polly configured via Startup.ConfigureServices() can be tested.
ConfigureServices
Polly policy is configured within it
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient<IHttpClientService, HttpClientService>()
.SetWaitAndRetryPolicy1();
}
}
Below is the Polly policy:
public static class IServiceCollectionExtension
{
public static void SetWaitAndRetryPolicy1(this IHttpClientBuilder clientBuilder)
{
clientBuilder.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)),
onRetry: (outcome, timespan, retryCount, context) =>
{
service.GetService<ILog>().Error("Delaying for {delay}ms, then making retry {retry}.",
timespan.TotalMilliseconds, retryCount);
}
)
);
}
}
Below is what I tried:
Integration test
The Polly policy is configured within the test.
public class RetryPolicyTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _factory;
public RetryPolicyTests(WebApplicationFactory<Startup> factory)
{
_factory = factory;
}
[Theory]
[InlineData("http://localhost:1234/api/v1/car/")]
public async Task Test3(string url)
{
// Arrange
var client = _factory.WithWebHostBuilder(whb =>
{
whb.ConfigureServices((bc, sc) =>
{
sc.AddOptions();
sc.AddHttpClient("test")
.SetWaitAndRetryPolicy1(); //Test the Polly policy
sc.BuildServiceProvider();
});
})
.CreateClient(); //cannot get a named or typed HttpClient
// Act
var body = "{}";
using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
{
var response = await client.PostAsync(url, content);
}
//Assert: somewhy assert it
}
}
}
The problem is that
I cannot retrieve the HttpClient
that has been configured with the Polly polly. Because WebApplicationFactory.CreateClient()
has no overloads that returns a named or typed HttpClient
:
Any idea?
Is there a better way to testing it?
ASPS.NET Core API 2.2
Upvotes: 2
Views: 3778
Reputation: 8156
To modify your posted code minimally to obtain the named or typed HttpClient
configured on HttpClientFactory, build the IServiceProvider
, obtain the IHttpClientFactory
and then obtain the configured client from IHttpClientFactory
.
var configuredClient = sc.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient("test");
Many people consider the use of IServiceProvider
like this to be a service-locator anti-pattern in production code; perhaps it is ok here in a test, to pull the specific item you want to unit-test out of the default app configuration. However, there are also shorter ways for a test to get a sample HttpClient
configured on HttpClientFactory, without using a full WebApplicationFactory
(see last part of answer).
For a full end-to-end integration test, testing how your app uses the configured policy, using WebApplicationFactory
to exercise some endpoint of your app like http://localhost:1234/api/v1/car/
:
You could - within the integration test - use a tool like Mountebank for .NET or HttpClientInterception to stub out the calls that the configured HttpClient
makes, so that those calls return errors which you expect the policy to handle.
You could use the ability of WebHostBuilder.ConfigureServices(...)
to modify the normal startup of your app, to make it easy to assert something to prove the policy was called. For example, you could configure a mock/fake ILog
implementation, and assert that the ILog.Error(...)
call in your onRetry
delegate takes place.
For the shortest-possible, self-contained unit test to test a Polly policy configured on a given HttpClient
configuration on HttpClientFactory, you could use a code pattern like below. This only uses IHttpClientFactory and the standard Microsoft DI infrastructure; no web host from ASP.NET.
public class HttpClientFactory_Polly_Policy_Test
{
[Fact]
public async Task Given_a_retry_policy_configured_on_a_named_client_When_call_via_the_named_client_Then_the_policy_is_used()
{
// Given / Arrange
IServiceCollection services = new ServiceCollection();
bool retryCalled = false;
HttpStatusCode codeHandledByPolicy = HttpStatusCode.InternalServerError;
const string TestClient = "TestClient";
services.AddHttpClient(TestClient)
.AddPolicyHandler(HttpPolicyExtensions.HandleTransientHttpError()
.RetryAsync(3, onRetry: (_, __) => retryCalled = true))
.AddHttpMessageHandler(() => new StubDelegatingHandler(codeHandledByPolicy));
HttpClient configuredClient =
services
.BuildServiceProvider()
.GetRequiredService<IHttpClientFactory>()
.CreateClient(TestClient);
// When / Act
var result = await configuredClient.GetAsync("https://www.doesnotmatterwhatthisis.com/");
// Then / Assert
Assert.Equal(codeHandledByPolicy, result.StatusCode);
Assert.True(retryCalled);
}
}
public class StubDelegatingHandler : DelegatingHandler
{
private readonly HttpStatusCode stubHttpStatusCode;
public StubDelegatingHandler(HttpStatusCode stubHttpStatusCode) => this.stubHttpStatusCode = stubHttpStatusCode;
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(stubHttpStatusCode));
}
If the declarations of policies are pulled out to methods (like SetWaitAndRetryPolicy1()
in your posted code), an approach like above provides a more unit-test-focused way to test them.
Upvotes: 1