使用 Polly 进行 HttpClient 单元测试

Dav*_*eux 4 c# unit-testing dotnet-httpclient polly retry-logic

我正在寻找HttpClient对 a进行单元测试Polly RetryPolicy,并且我正在尝试找出如何控制响应的内容HTTP

我在客户端上使用了 a HttpMessageHandler,然后覆盖发送异步,这效果很好,但是当我添加 Polly 重试策略时,我必须使用 a 创建 HTTP 客户端的实例IServiceCollection,并且无法HttpMessageHandler为客户端创建 a。我尝试过使用,.AddHttpMessageHandler()但这会阻止轮询重试策略,并且只会触发一次。

这就是我在测试中设置 HTTP 客户端的方式

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
}
Run Code Online (Sandbox Code Playgroud)

然后,当我调用时,这将触发请求_httpClient.SendAsync(httpRequestMessage),但它实际上创建了一个对地址的 Http 调用,我需要以某种方式拦截此请求并返回受控响应。

我想测试该策略是否用于在请求失败时重试请求,并在完整响应时完成。

我的主要限制是我不能在 MSTest 上使用 Moq。

sim*_*son 9

您不希望HttpClient在单元测试中发出真正的 HTTP 请求 - 这将是集成测试。为了避免提出真正的请求,您需要提供自定义的HttpMessageHandler. 您在帖子中规定您不想使用模拟框架,因此HttpMessageHandler您可以提供一个存根,而不是模拟。

由于评论对 Polly 的 GitHub 页面上的一个问题产生了重大影响,我调整了您的示例以调用存根HttpMessageHandler,该存根在第一次调用时抛出 500,然后在后续请求中返回 200。

该测试断言重试处理程序已被调用,并且当执行步骤超过对HttpClient.SendAsync结果响应的调用时,状态代码为 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));
    }
}
Run Code Online (Sandbox Code Playgroud)