在 IHttpClientFactory 注入时模拟 HttpClient 处理程序

Mocking a HttpClient handler when injected by IHttpClientFactory

我创建了一个自定义库,它会自动为依赖于 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 依赖项中。

Note
There are many answers to be found online and on Whosebug where the suggestion is to construct the HttpClient yourself, i.e.: new HttpClient(new MyMockedHandler());, but this defeats my purpose of needing to test if the actual IHttpClientFactory is constructing httpclients with the requested policies.

为此,我想用 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 响应。

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

我们最终创建了几个集成测试。我们已经使用 WireMock.NET 创建了一个服务器存根。所以,这一切的重点是让 ASP.NET DI 发挥它的魔力,然后仔细检查存根的日志。

我们创建了两个基础 classes 来包装 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<Startup> 派生的实例 class)

[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 } }));
}

希望对您有所帮助。