理解、創建、使用和測試HttpClient

目錄

介紹

背景

使用代碼

興趣點


介紹

HttpClient是經常使用,但也往往不能完全理解。它的行爲可能受到DelegationHandler實現的影響,可以通過依賴注入來使用實例,並且可以通過集成測試來測試它的工作方式。本文介紹了這些事情如何工作。

背景

本文適用HttpClient至少使用過一次並希望瞭解更多信息的.NET Core開發人員。

使用代碼

首先,我們要設置HttpClient的創建和依賴注入。在ASP.NET Core應用程序中,這通常是在ConfigureServices方法中完成的。一個HttpClient實例通過依賴注入被注入到一個SearchEngineService實例。兩個處理程序管理HttpClientLogHandlerRetryHandler的行爲。這是ConfigureServices實現的樣子:

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers();
   services.AddTransient<LogHandler>();
   services.AddTransient<RetryHandler>();
   var googleLocation = Configuration["Google"];
   services.AddHttpClient<ISearchEngineService, SearchEngineService>(c =>
   {
       c.BaseAddress = new Uri(googleLocation);
   }).AddHttpMessageHandler<LogHandler>()
       .AddHttpMessageHandler<RetryHandler>();
}

從上面的代碼可以清楚地看到,LogHandler設置在RetryHandler之前。LogHandler是第一個處理程序,因此它處理在調用HttpClient時需要直接發生的事情這是LogHandler實現:

public class LogHandler : DelegatingHandler
{
    private readonly ILogger<LogHandler> _logger;

    public LogHandler(ILogger<LogHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> 
      SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);
        _logger.LogInformation("{response}", response);
        return response;
    }
}

從上面的代碼可以清楚地看到,此處理程序實現僅在調用base方法之後記錄來自Web請求的響應。此基本方法觸發的內容由第二個處理程序設置:RetryHandler。如果服務器意外錯誤,此處理程序將重試。如果它直接成功或給出3次以上的服務器錯誤,則最後的結果計數並將返回。

public class RetryHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> 
      SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage result = null;
        for (int i = 0; i < 3; i++)
        {
            result = await base.SendAsync(request, cancellationToken);
            if (result.StatusCode >= HttpStatusCode.InternalServerError)
            {
                continue;
            }
            return result;
        }
        return result;
    }
}

如前所述,需要將這些處理程序管理的HttpClient注入到SearchEngineService實例中。這個類只有一個方法。該方法調用HttpClient實例,並返回內容的長度作爲響應。

public class SearchEngineService : ISearchEngineService
{
    private readonly HttpClient _httpClient;

    public SearchEngineService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<int> GetNumberOfCharactersFromSearchQuery(string toSearchFor)
    {
        var result = await _httpClient.GetAsync($"/search?q={toSearchFor}");
        var content = await result.Content.ReadAsStringAsync();
        return content.Length;
    }
}

SearchEngineService是控制器類的依賴,這個控制器類有一個get方法,它將方法調用的結果作爲ActionResult返回這是控制器類。

[Route("api/[controller]")]
[ApiController]
public class SearchEngineController : ControllerBase
{
    private readonly ISearchEngineService _searchEngineService;

    public SearchEngineController(ISearchEngineService searchEngineService)
    {
        _searchEngineService = searchEngineService;
    }

    [HttpGet("{queryEntry}", Name = "GetNumberOfCharacters")]
    public async Task<ActionResult<int>> GetNumberOfCharacters(string queryEntry)
    {
        var numberOfCharacters = 
            await _searchEngineService.GetNumberOfCharactersFromSearchQuery(queryEntry);
        return Ok(numberOfCharacters);
    }
}

要編寫一個集成測試此控制器,我們使用IntegrationFixtureNuGet這裏,文檔在這裏,文章有一些類似的代碼在這裏)。外部依賴關係已由模擬服務器代替,該模擬服務器在第一個請求之後返回內部服務器錯誤,而在第二個請求之後成功。對我們的控制器方法的調用已完成。這會觸發對SearchEngineService的調用,從而調用HttpClient。如前所述,此類調用觸發對LogHandler的調用,此後觸發對RetryHandler的調用。由於第一個調用給出服務器錯誤,因此重試完成。RetryHandler不觸發LogHandler(反之亦然)。因此,我們的應用程序僅記錄一個響應,而實際上有兩個響應(一個失敗和一個成功)。這是我們的集成測試的代碼:

[Fact]
public async Task TestDelegate()
{
    // arrange
    await using (var fixture = new Fixture<Startup>())
    {
        using (var searchEngineServer = fixture.FreezeServer("Google"))
        {
            SetupUnStableServer(searchEngineServer, "Response");
            var controller = fixture.Create<SearchEngineController>();

            // act
            var response = await controller.GetNumberOfCharacters("Hoi");

            // assert, external
            var externalResponseMessages = 
            searchEngineServer.LogEntries.Select(l => l.ResponseMessage).ToList();
            Assert.Equal(2, externalResponseMessages.Count);
            Assert.Equal((int)HttpStatusCode.InternalServerError, 
                        externalResponseMessages.First().StatusCode);
            Assert.Equal((int)HttpStatusCode.OK, externalResponseMessages.Last().StatusCode);

            // assert, internal
            var loggedResponse = 
               fixture.LogSource.GetLoggedObjects<HttpResponseMessage>().ToList();
            Assert.Single(loggedResponse);
            var externalResponseContent = 
               await loggedResponse.Single().Value.Content.ReadAsStringAsync();
            Assert.Equal("Response", externalResponseContent);
            Assert.Equal(HttpStatusCode.OK, loggedResponse.Single().Value.StatusCode);
            Assert.Equal(8, ((OkObjectResult)response.Result).Value);
        }
    }
}

private void SetupUnStableServer(FluentMockServer fluentMockServer, string response)
{
    fluentMockServer.Given(Request.Create().UsingGet())
        .InScenario("UnstableServer")
        .WillSetStateTo("FIRSTCALLDONE")
        .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
            .WithStatusCode(HttpStatusCode.InternalServerError));

    fluentMockServer.Given(Request.Create().UsingGet())
        .InScenario("UnstableServer")
        .WhenStateIs("FIRSTCALLDONE")
        .RespondWith(Response.Create().WithBody(response, encoding: Encoding.UTF8)
            .WithStatusCode(HttpStatusCode.OK));
}

如果查看上面顯示的代碼,則會看到兩個斷言部分。在第一個斷言部分中,我們驗證外部(模擬)服務器的日誌。由於第一個Web請求失敗。我們希望執行第二個Web請求(帶有第二個響應),因此應該有兩個響應,這正是我們驗證的結果。

在第二個斷言部分中,我們驗證應用程序本身的日誌。如前所述,僅記錄了一個響應,因此我們在第二個斷言部分對此進行了驗證。

如果您想進一步熟悉,建議在本文中顯示的GitHub下載源代碼。例如,您可以更改處理程序的順序或添加新的處理程序,然後看看會發生什麼。通過使用IntegrationFixture進行測試,您可以輕鬆地驗證我們自己的應用程序和外部(模擬)服務器的日誌。

興趣點

在撰寫本文和示例代碼時,我對HttpClient實際的工作方式有了更好的瞭解。通過使用處理程序,您不僅可以執行Web請求,還可以做更多的事情。您可以在進行Web請求時構建日誌記錄,重試機制或其他所需的內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章