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





5.00/5 (1投票)
描述如何让 HttpClient 客户端按照您的意愿执行操作,并使用 xUnit 进行测试。
引言
HttpClient
类经常被使用,但也常常没有被完全理解。它的行为可以通过 DelegationHandler
实现来影响,实例可以通过依赖注入来使用,以及它的工作方式可以通过集成测试来测试。本文介绍了这些事情是如何工作的。
背景
本文适用于至少使用过一次 HttpClient
并且想了解更多关于它的 .NET Core 开发者。
Using the Code
首先,我们希望设置 HttpClient
的创建和依赖注入。在 ASP.NET Core 应用程序中,这通常在 ConfigureServices
方法中完成。一个 HttpClient
实例必须通过依赖注入注入到 SearchEngineService
实例中。两个处理程序管理 HttpClient
的行为:LogHandler
和 RetryHandler
。 这是 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 日:初始版本