2

我创建了一个自定义库,它自动为特定服务设置 Polly 策略,这些服务依赖于HttpClient.

这是使用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自己构建,即:new HttpClient(new MyMockedHandler());,但这违背了我需要测试实际IHttpClientFactory是否使用请求的策略构建 httpclients 的目的。

为此,我想使用由real生成的real 进行测试,但我希望模拟它的处理程序,这样我就可以避免发出实际的 Web 请求并人为地导致错误的响应。HttpClient 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 响应。

4

1 回答 1

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

故障服务器

(我们已经使用场景来模拟超时)

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

慢速处理的简单测试

(是派生类proxyApiInitializer的一个实例)WebApplicationFactory<Startup>

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

我希望这可以帮助你。

于 2020-07-23T07:32:33.347 回答