首页 > 解决方案 > 在 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 。由框架自动注入,无需显式注册。HttpClientServiceHttpClientHttpClient

 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不能模拟HttpClientServicePostAsync方法。我可以将其更改为虚拟,但我想知道它是否是最佳选择。

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

单元测试:类似于HttpClientServiceMoq不能模拟HttpClientPostAsync方法。

  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的解决方案

标签: c#unit-testingasp.net-core.net-coreasp.net-core-webapi

解决方案


假设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完成测试。

控制器需要确保安排其他依赖项以使测试顺利进行,但这目前超出了问题的范围。


推荐阅读