【问题标题】:Mocking a HttpClient handler when injected by IHttpClientFactory由 IHttpClientFactory 注入时模拟 HttpClient 处理程序
【发布时间】:2020-07-22 10:20:36
【问题描述】:

我创建了一个自定义库,它可以为依赖于 HttpClient 的特定服务自动设置 Polly 策略。

这是使用IServiceCollection 扩展方法和类型化客户端方法完成的。一个简化的例子:

public static IHttpClientBuilder SetUpFooServiceHttpClient(this IServiceCollection services)
{
    return services
            .AddHttpClient<FooService>()
            .AddPolicyHandler(GetRetryPolicy());
}

public class FooService
{
    private readonly HttpClient _client;

    public FooService(HttpClient httpClient)
    {
        _client = httpClient;
    }

    public void DoJob()
    {
         var test = _client.GetAsync("http://example.com");
    }
}

请注意,我的真实代码使用泛型类型和选项生成器,但为了简单起见,我省略了该部分。我的测试的目的是确认我的选项生成器正确地应用了我希望它应用的策略。为了此处的示例,我们假设这是我要测试的硬编码重试策略。

我现在想测试这个库是否正确地将 Polly 策略注册到我注入的 HttpClient 依赖项中。

注意
网上和 StackOverflow 上有很多答案,建议您自己构建 HttpClient,即:@ 987654326@,但这违背了我需要测试实际 IHttpClientFactory 是否正在使用请求的策略构造 httpclients 的目的。

为此,我想用 real HttpClient 进行测试,它由 real IHttpClientFactory 生成,但我希望它的处理程序被模拟我可以避免发出实际的网络请求并人为地导致错误的响应。

我正在使用AddHttpMessageHandler() 注入一个模拟处理程序,但工厂似乎忽略了这一点。

这是我的测试夹具:

public class BrokenDelegatingHandler : DelegatingHandler
{
    public int SendAsyncCount = 0;
    
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        SendAsyncCount++;
        
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError));
    }
}

private BrokenDelegatingHandler _brokenHandler = new BrokenDelegatingHandler();

private FooService GetService()
{
    var services = new ServiceCollection();

    services.AddTransient<BrokenDelegatingHandler>();
        
    var httpClientBuilder = services.SetUpFooServiceHttpClient();

    httpClientBuilder.AddHttpMessageHandler(() => _brokenHandler);
        
    services.AddSingleton<FooService>();
        
    return services
            .BuildServiceProvider()
            .GetRequiredService<FooService>();
}

这是我的测试:

[Fact]
public void Retries_client_connection()
{
    int retryCount = 3;
    
    var service = GetService();

    _brokenHandler.SendAsyncCount.Should().Be(0); // PASS
    
    var result = service.DoJob();

    _brokenHandler.SendAsyncCount.Should().Be(retryCount); // FAIL: expected 3 but got 0
}

当我调试测试时,处理程序的断点永远不会被命中,并且响应返回为 200(因为它实际上连接到 URL,而不是命中模拟的处理程序)。

为什么 http 客户端工厂忽略了我的模拟处理程序?

请注意,我也将接受任何允许我以另一种有效方式测试策略的答案。

我知道我可以只使用损坏的 URL 字符串,但我需要在测试中测试特定的 http 响应。

【问题讨论】:

    标签: c# unit-testing mocking polly httpclientfactory


    【解决方案1】:

    几个月前我们遇到了类似的问题。如何测试注入的HttpClient 是否使用正确的策略进行装饰。 (我们使用了 Retry > CircuitBreaker > Timeout 策略链)。

    我们最终创建了几个集成测试。我们使用WireMock.NET 创建了一个服务器存根。所以,这样做的全部意义在于让 ASP.NET DI 发挥作用,然后检查存根的日志。

    我们创建了两个封装 WireMock 设置的基类(我们有一个 POST 端点)。

    完美服务器

    internal abstract class FlawlessServiceMockBase
    {
        protected readonly WireMockServer server;
        private readonly string route;
    
        protected FlawlessServiceMockBase(WireMockServer server, string route)
        {
            this.server = server;
            this.route = route;
        }
    
        public virtual void SetupMockForSuccessResponse(IResponseBuilder expectedResponse = null, 
            HttpStatusCode expectedStatusCode = HttpStatusCode.OK)
        {
            server.Reset();
    
            var endpointSetup = Request.Create().WithPath(route).UsingPost();
            var responseSetup = expectedResponse ?? Response.Create().WithStatusCode(expectedStatusCode);
    
            server.Given(endpointSetup).RespondWith(responseSetup);
        }
    }
    

    故障服务器

    (我们使用scenarios来模拟超时)

    internal abstract class FaultyServiceMockBase
    {
        protected readonly WireMockServer server;
        protected readonly IRequestBuilder endpointSetup;
        protected readonly string scenario;
    
        protected FaultyServiceMockBase(WireMockServer server, string route)
        {
            this.server = server;
            this.endpointSetup = Request.Create().WithPath(route).UsingPost();
            this.scenario = $"polly-setup-test_{this.GetType().Name}";
        }
    
        public virtual void SetupMockForFailedResponse(IResponseBuilder expectedResponse = null,
            HttpStatusCode expectedStatusCode = HttpStatusCode.InternalServerError)
        {
            server.Reset();
    
            var responseSetup = expectedResponse ?? Response.Create().WithStatusCode(expectedStatusCode);
    
            server.Given(endpointSetup).RespondWith(responseSetup);
        }
    
        public virtual void SetupMockForSlowResponse(ResilienceSettings settings, string expectedResponse = null)
        {
            server.Reset();
    
            int higherDelayThanTimeout = settings.HttpRequestTimeoutInMilliseconds + 500;
    
            server
                .Given(endpointSetup)
                .InScenario(scenario)
                //NOTE: There is no WhenStateIs
                .WillSetStateTo(1)
                .WithTitle(Common.Constants.Stages.Begin)
                .RespondWith(DelayResponse(higherDelayThanTimeout, expectedResponse));
    
            for (var i = 1; i < settings.HttpRequestRetryCount; i++)
            {
                server
                    .Given(endpointSetup)
                    .InScenario(scenario)
                    .WhenStateIs(i)
                    .WillSetStateTo(i + 1)
                    .WithTitle($"{Common.Constants.Stages.RetryAttempt} #{i}")
                    .RespondWith(DelayResponse(higherDelayThanTimeout, expectedResponse));
            }
    
            server
                .Given(endpointSetup)
                .InScenario(scenario)
                .WhenStateIs(settings.HttpRequestRetryCount)
                //NOTE: There is no WillSetStateTo
                .WithTitle(Common.Constants.Stages.End)
                .RespondWith(DelayResponse(1, expectedResponse));
        }
    
        private static IResponseBuilder DelayResponse(int delay) => Response.Create()
            .WithDelay(delay)
            .WithStatusCode(200);
    
        private static IResponseBuilder DelayResponse(int delay, string response) => 
            response == null 
                ? DelayResponse(delay) 
                : DelayResponse(delay).WithBody(response);
    }
    

    慢速处理的简单测试

    proxyApiInitializerWebApplicationFactory&lt;Startup&gt; 派生类的一个实例)

    [Fact]
    public async Task GivenAValidInout_AndAServiceWithSlowProcessing_WhenICallXYZ_ThenItCallsTheServiceSeveralTimes_AndFinallySucceed()
    {
        //Arrange - Proxy request
        HttpClient proxyApiClient = proxyApiInitializer.CreateClient();
        var input = new ValidInput();
    
        //Arrange - Service
        var xyzSvc = new FaultyXYZServiceMock(xyzServer.Value);
        xyzSvc.SetupMockForSlowResponse(resilienceSettings);
    
        //Act
        var actualResult = await CallXYZAsync(proxyApiClient, input);
    
        //Assert - Response
        const HttpStatusCode expectedStatusCode = HttpStatusCode.OK;
        actualResult.StatusCode.ShouldBe(expectedStatusCode);
    
        //Assert - Resilience Policy
        var logsEntries = xyzServer.Value.FindLogEntries(
            Request.Create().WithPath(Common.Constants.Routes.XYZService).UsingPost());
        logsEntries.Last().MappingTitle.ShouldBe(Common.Constants.Stages.End);
    }
    

    XYZ 服务器初始化:

    private static Lazy<WireMockServer> xyzServer;
    
    public ctor()
    {
       xyzServer = xyzServer ?? InitMockServer(API.Constants.EndpointConstants.XYZServiceApi);
    }
    
    private Lazy<WireMockServer> InitMockServer(string lookupKey)
    {
        string baseUrl = proxyApiInitializer.Configuration.GetValue<string>(lookupKey);
        return new Lazy<WireMockServer>(
            WireMockServer.Start(new FluentMockServerSettings { Urls = new[] { baseUrl } }));
    }
    

    希望对你有帮助。

    【讨论】:

    • +1 对于似乎是解决问题的有效方法,但老实说,我有点担心采用这条路线,因为它是实际服务器进程的相当广泛的实现,本质上是需要是具有硬编码响应的模拟处理程序。
    • @Flater 我同意你的观点,这很重。我很高兴看到某种可以验证策略的组件级测试。不幸的是,如果我们能够以某种方式检索我们无法评估其属性的策略。我们无法确定策略是否按预期方式设置。我已经在 Polly 上提出了issue 来解决这个不足。 V8 将以重新设计的配置发布。
    • 根据我的发现,AddHttpMessageHandler 应该能够解决这个问题,因为它会为 polly 处理程序添加一个内部处理程序,从而允许您获得一个由 真正的 polly 处理程序。 MSDN 确认 AddHttpMessageHandler 应该按照我期望的方式工作,但 DI 容器似乎完全忽略了我调用它的事实。
    • 我已经能够缩小问题的范围。注入的IHttpClientFactory 尊重我的自定义处理程序(包括Polly),但注入的HttpClient 没有(它也忽略Polly 策略)。这似乎与我在该主题上找到的文档相矛盾,但我将在另一个问题中探讨这一点。我给你打勾是因为你的答案是一个有效的解决方案,但我不会使用它,因为它对我的用例来说有点重。
    • The new question,供参考:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多