【问题标题】:Unit Testing Core API Controller Using Custom HttpClient and Polly policy within ConfigureServices在 ConfigureServices 中使用自定义 HttpClient 和 Polly 策略对核心 API 控制器进行单元测试
【发布时间】:2018-12-28 12:11:56
【问题描述】:

在使用 PollyHttpClient 时,我在执行单元测试时遇到问题。

具体来说,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)在底部指定。我想知道这是否是使用PollyHttpClient 的正确方法。

配置服务

配置 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

类似于CarControllerHttpClientService 存在单元测试问题,因为无法模拟HttpClient 的 PostAsync。 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 无法模拟 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 Core API 2.2

更新

将 CustomHttpClient 的错字更正为 HttpClientService

更新 2

Solution到问题2

【问题讨论】:

  • 控制器与实现问题紧密耦合,正如您已经体验过的那样,很难单独测试(单元测试)。将那些在测试时可以替换的抽象封装起来。
  • 在这个例子中 CustomHttpClientHttpClientService 吗?错别字?
  • 更正了错字,请查看我的更新并对您的回答发表评论。谢谢。

标签: c# unit-testing asp.net-core .net-core asp.net-core-webapi


【解决方案1】:

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

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

【讨论】:

  • 很好,HttpClientServiceTests 呢?这是由于 HttpClient 无法被 Moq 模拟造成的问题 2。
  • @Pingpong 评论 stackoverflow.com/a/44716910/5233410 获取有关 HttpClient 依赖项测试的帮助
猜你喜欢
  • 1970-01-01
  • 2011-10-25
  • 1970-01-01
  • 1970-01-01
  • 2020-10-29
  • 2017-06-02
  • 1970-01-01
  • 1970-01-01
  • 2018-06-17
相关资源
最近更新 更多