c# - 在 ConfigureServices 中使用自定义 HttpClient 和 Polly 策略对核心 API 控制器进行单元测试
问题描述
Polly
使用and时执行单元测试时遇到问题HttpClient
。
具体来说,Polly 和 HttpClient 用于 ASP.NET Core Web API 控制器,链接如下:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests
https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory
问题(1 和 2)在底部指定。Polly
我想知道这是否是使用and的正确方法HttpClient
。
配置服务
配置 Polly 策略并指定自定义 HttpClient,CustomHttpClient
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddHttpClient<HttpClientService>()
.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
);
}
汽车控制器
CarController 依赖于HttpClientService
,由框架自动注入,无需显式注册。
请注意,这HttpClientService
会导致单元测试出现问题,因为Moq
无法模拟非虚拟方法,这将在后面提到。
[ApiVersion("1")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class CarController : ControllerBase
{
private readonly ILog _logger;
private readonly HttpClientService _httpClientService;
private readonly IOptions<Config> _config;
public CarController(ILog logger, HttpClientService httpClientService, IOptions<Config> config)
{
_logger = logger;
_httpClientService = httpClientService;
_config = config;
}
[HttpPost]
public async Task<ActionResult> Post()
{
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
string body = reader.ReadToEnd();
var statusCode = await _httpClientService.PostAsync(
"url",
new Dictionary<string, string>
{
{"headerID", "Id"}
},
body);
return StatusCode((int)statusCode);
}
}
}
HttpClientService
与 类似,单元测试存在问题,因为CarController
无法模拟 的 PostAsync 。由框架自动注入,无需显式注册。HttpClientService
HttpClient
HttpClient
public class HttpClientService
{
private readonly HttpClient _client;
public HttpClientService(HttpClient client)
{
_client = client;
}
public async Task<HttpStatusCode> PostAsync(string url, Dictionary<string, string> headers, string body)
{
using (var content = new StringContent(body, Encoding.UTF8, "application/json"))
{
foreach (var keyValue in headers)
{
content.Headers.Add(keyValue.Key, keyValue.Value);
}
var response = await _client.PostAsync(url, content);
response.EnsureSuccessStatusCode();
return response.StatusCode;
}
}
问题 1
单元测试:Moq
不能模拟HttpClientService
的PostAsync
方法。我可以将其更改为虚拟,但我想知道它是否是最佳选择。
public class CarControllerTests
{
private readonly Mock<ILog> _logMock;
private readonly Mock<HttpClient> _httpClientMock;
private readonly Mock<HttpClientService> _httpClientServiceMock;
private readonly Mock<IOptions<Config>> _optionMock;
private readonly CarController _sut;
public CarControllerTests() //runs for each test method
{
_logMock = new Mock<ILog>();
_httpClientMock = new Mock<HttpClient>();
_httpClientServiceMock = new Mock<HttpClientService>(_httpClientMock.Object);
_optionMock = new Mock<IOptions<Config>>();
_sut = new CarController(_logMock.Object, _httpClientServiceMock.Object, _optionMock.Object);
}
[Fact]
public void Post_Returns200()
{
//System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member
_httpClientServiceMock.Setup(hc => hc.PostAsync(It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(),
It.IsAny<string>()))
.Returns(Task.FromResult(HttpStatusCode.OK));
}
}
}
问题 2
单元测试:类似于HttpClientService
, Moq
不能模拟HttpClient
的PostAsync
方法。
public class HttpClientServiceTests
{
[Fact]
public void Post_Returns200()
{
var httpClientMock = new Mock<HttpClient>();
//System.NotSupportedException : Invalid setup on a non-virtual (overridable in VB) member
httpClientMock.Setup(hc => hc.PostAsync("", It.IsAny<HttpContent>()))
.Returns(Task.FromResult(new HttpResponseMessage()));
}
}
ASP.NET 核心 API 2.2
更新
将 CustomHttpClient 的错字更正为 HttpClientService
更新 2
问题2的解决方案
解决方案
假设CustomHttpClient
是一个错字并且HttpClientService
是实际的依赖项,控制器与实现问题紧密耦合,正如您已经体验过的那样,很难单独测试(单元测试)。
将这些具体封装在抽象背后
public interface IHttpClientService {
Task<HttpStatusCode> PostAsync(string url, Dictionary<string, string> headers, string body);
}
public class HttpClientService : IHttpClientService {
//...omitted for brevity
}
可以在测试时更换。
重构控制器以依赖于抽象而不是具体的实现
public class CarController : ControllerBase {
private readonly ILog _logger;
private readonly IHttpClientService httpClientService; //<-- TAKE NOTE
private readonly IOptions<Config> _config;
public CarController(ILog logger, IHttpClientService httpClientService, IOptions<Config> config) {
_logger = logger;
this.httpClientService = httpClientService;
_config = config;
}
//...omitted for brevity
}
更新服务配置以使用允许注册抽象及其实现的重载
public void ConfigureServices(IServiceCollection services) {
services.AddHttpClient();
services
.AddHttpClient<IHttpClientService, HttpClientService>() //<-- TAKE NOTE
.AddPolicyHandler((service, request) =>
HttpPolicyExtensions.HandleTransientHttpError()
.WaitAndRetryAsync(3,
retryCount => TimeSpan.FromSeconds(Math.Pow(2, retryCount)))
);
}
这需要重新编写代码以使其对测试更加友好。
下面显示了控制器现在如何单独进行单元测试
public class CarControllerTests {
private readonly Mock<ILog> _logMock;
private readonly Mock<IHttpClientService> _httpClientServiceMock;
private readonly Mock<IOptions<Config>> _optionMock;
private readonly CarController _sut;
public CarControllerTests() //runs for each test method
{
_logMock = new Mock<ILog>();
_httpClientServiceMock = new Mock<IHttpClientService>();
_optionMock = new Mock<IOptions<Config>>();
_sut = new CarController(_logMock.Object, _httpClientServiceMock.Object, _optionMock.Object);
}
[Fact]
public async Task Post_Returns200() {
//Arrange
_httpClientServiceMock
.Setup(_ => _.PostAsync(
It.IsAny<string>(),
It.IsAny<Dictionary<string, string>>(),
It.IsAny<string>())
)
.ReturnsAsync(HttpStatusCode.OK);
//Act
//...omitted for brevity
//...
}
}
请注意如何不再需要HttpClient
完成测试。
控制器需要确保安排其他依赖项以使测试顺利进行,但这目前超出了问题的范围。
推荐阅读
- python - 如何通过 Telepot 在机器人电报中获取用户的位置和地点
- search - search-forward 和其他搜索函数的返回类型是什么?
- android - WiFi 建议 API 在 Android 上失败
- javascript - 了解 Svelte tick() 生命周期
- php - 如何通过MYSQL在按钮单击上切换布尔值
- if-statement - 未来
颤振中的 vs 布尔值 - firebase - Firebase 函数 onUpdate 触发器 - SendGrid“禁止”发送电子邮件时出错
- r - 有没有办法用 plotly 指定旭日形图中各部分的顺序?
- java - 是否可以在 Java 中创建具有多个角色的 MongoDb 用户?
- c# - 将输入写入进程 - RedirectStandardInput 不起作用