65.9K
CodeProject 正在变化。 阅读更多。
Home

了解、创建、使用和测试 HttpClient

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2020年6月2日

CPOL

3分钟阅读

viewsIcon

5923

描述如何让 HttpClient 客户端按照您的意愿执行操作,并使用 xUnit 进行测试。

引言

HttpClient 类经常被使用,但也常常没有被完全理解。它的行为可以通过 DelegationHandler 实现来影响,实例可以通过依赖注入来使用,以及它的工作方式可以通过集成测试来测试。本文介绍了这些事情是如何工作的。

背景

本文适用于至少使用过一次 HttpClient 并且想了解更多关于它的 .NET Core 开发者。

Using the Code

首先,我们希望设置 HttpClient 的创建和依赖注入。在 ASP.NET Core 应用程序中,这通常在 ConfigureServices 方法中完成。一个 HttpClient 实例必须通过依赖注入注入到 SearchEngineService 实例中。两个处理程序管理 HttpClient 的行为:LogHandlerRetryHandler。 这是 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;
    }
}

从上面的代码可以清楚地看出,这个处理程序实现只是在调用基本方法后记录来自 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);
    }
}

为了为此控制器编写集成测试,我们使用 IntegrationFixture(NuGet 包在这里,文档在这里,包含一些类似代码的文章在这里)。 外部依赖项被模拟服务器替换,该模拟服务器在第一次请求后返回内部服务器错误,并在第二次请求后成功。 对我们的控制器方法进行调用。 这会触发对 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 请求时需要的任何其他内容。

历史

  • 2020 年 5 月 31 日:初始版本
© . All rights reserved.