C# 在ConfigureServices中使用自定义HttpClient和Polly策略对核心API控制器进行单元测试
使用C# 在ConfigureServices中使用自定义HttpClient和Polly策略对核心API控制器进行单元测试,c#,unit-testing,asp.net-core,.net-core,asp.net-core-webapi,C#,Unit Testing,Asp.net Core,.net Core,Asp.net Core Webapi,使用Polly和HttpClient时,我在执行单元测试时遇到问题 具体而言,Polly和HttpClient用于ASP.NET核心Web API控制器,链接如下: 问题(1和2)在底部指定。我想知道这是否是使用Polly和HttpClient的正确方法 配置服务 配置Polly策略并单独指定自定义HttpClient,CustomHttpClient public void ConfigureServices(IServiceCollection services) { serv
Polly
和HttpClient
时,我在执行单元测试时遇到问题
具体而言,Polly和HttpClient用于ASP.NET核心Web API控制器,链接如下:
问题(1和2)在底部指定。我想知道这是否是使用Polly
和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)))
);
}
HttpClientService
与CarController
类似,HttpClientService
在单元测试方面存在问题,因为无法模拟HttpClient
的PostAsyncHttpClient
由框架自动注入,无需显式注册
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;
}
}
问题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()));
}
}
公共类HttpClientServiceTests
{
[事实]
公共无效邮政返回200()
{
var httpClientMock=new Mock();
//System.NotSupportedException:非虚拟(在VB中可重写)成员上的设置无效
httpClientMock.Setup(hc=>hc.PostAsync(“,It.IsAny()))
.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
}
公共接口IHttpClientService{
任务PostAsync(字符串url、字典标题、字符串正文);
}
公共类HttpClientService:IHttpClientService{
//…为简洁起见省略
}
可以在测试时替换
重构控制器,使其依赖于抽象,而不是具体的实现
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)))
);
}
公共类CarController:ControllerBase{
专用只读ILog_记录器;
私有只读IHttpClientService httpClientService;//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
//...
}
}
公共类CarControllerTests{
私有只读Mock_logMock;
私有只读模拟\u httpClientServiceMock;
私有只读Mock_optionMock;
专用只读CarController_sut;
public CarControllerTests()//为每个测试方法运行
{
_logMock=newmock();
_httpClientServiceMock=新建Mock();
_optionMock=newmock();
_sut=新的CarController(_logMock.Object、_httpClientServiceMock.Object、_optionMock.Object);
}
[事实]
公共异步任务Post_Returns200(){
//安排
_httpClientServiceMock
.Setup(=>.PostAsync(
It.IsAny(),
It.IsAny(),
It.IsAny())
)
.ReturnsAsync(HttpStatusCode.OK);
//表演
//…为简洁起见省略
//...
}
}
请注意,如何不再需要HttpClient
来完成测试
控制器需要确保为测试流程安排了其他依赖项,但这目前超出了问题的范围。控制器与实现问题紧密耦合,正如您已经经历过的,很难单独测试(单元测试)。将这些具体化封装在测试时可以替换的抽象后面。在本例中,
CustomHttpClient
是HttpClientService
吗?错别字?更正了错别字,请查看我的更新和对您答案的评论。谢谢。太好了,那HttpClientServiceTests呢?这是由于HttpClient无法被Moq模拟而导致的问题2。@Pingpong review可获得HttpClient依赖性测试的帮助
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
//...
}
}