理解、创建、使用和测试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请求时构建日志记录,重试机制或其他所需的内容。

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