Мокинг обработчика HttpClient при внедрении IHttpClientFactory

Я создал специальную библиотеку, которая автоматически настраивает политики 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 создает http-клиентов с запрошенными политиками.

С этой целью я хочу протестировать с настоящим 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-ответы в моих тестах.


person Flater    schedule 22.07.2020    source источник


Ответы (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 } }));
}

Я надеюсь это тебе поможет.

person Peter Csala    schedule 23.07.2020
comment
+1 Для того, что кажется допустимым решением проблемы, но, честно говоря, я немного опасаюсь идти по этому пути, поскольку это довольно обширная реализация реального серверного процесса для того, что, по сути, должно быть имитирующим обработчиком с жестко закодированный ответ. - person Flater; 23.07.2020
comment
@Flater Я согласен с вами, что это довольно тяжело. Я был бы рад увидеть какое-то тестирование на уровне компонентов, которое может проверить политики. К сожалению, если бы мы могли каким-то образом получить политики, мы не смогли бы оценить их свойства. Мы не можем быть уверены, что политики были настроены должным образом. Я уже поднимал проблему на Polly, чтобы устранить этот дефицит. V8 выйдет с переработанной конфигурацией. - person Peter Csala; 23.07.2020
comment
Основываясь на моих выводах, AddHttpMessageHandler должен быть в состоянии решить эту проблему, поскольку он добавит внутренний обработчик к обработчикам опросов, что позволит вам получить фиктивный результат, который обрабатывается настоящими обработчиками опросов. MSDN подтверждает, что AddHttpMessageHandler должен работать так, как я ожидал, но контейнер DI, похоже, полностью игнорирует тот факт, что я его вызвал. - person Flater; 23.07.2020
comment
Я смог сузить проблему. Введенный IHttpClientFactory уважает мои пользовательские обработчики (включая Polly), а внедренный HttpClient - нет (он также игнорирует политики Polly). Это, по-видимому, противоречит документации, которую я нашел по этому вопросу, но я исследую это в другом вопросе. Я ставлю галочку, так как ваш ответ - верное решение, но я не собираюсь его использовать, потому что это немного тяжеловато для моего варианта использования. - person Flater; 23.07.2020
comment
@Flater Спасибо и ждем этого другого вопроса: D - person Peter Csala; 23.07.2020
comment
Новый вопрос, для справки :) - person Flater; 23.07.2020