Unit-Testing Core API Controller Использование пользовательских политик HttpClient и Polly в ConfigureServices - PullRequest
0 голосов
/ 28 декабря 2018

У меня проблемы с выполнением модульного тестирования при использовании Polly и HttpClient.

В частности, Polly и HttpClient используются для ASP.NET Core Web API Controller по ссылкам ниже:

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests

https://github.com/App-vNext/Polly/wiki/Polly-and-HttpClientFactory

Проблемы (1 и 2) указаны внизу.Интересно, правильный ли это способ использования Polly и HttpClient.

ConfigureServices

Настройка политики 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

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, HttpClientService проблема с модульным тестом, потому что PostAsync 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 Core API 2.2

Обновление

Исправленоопечатка CustomHttpClient для HttpClientService

Обновление 2

Решение для проблемы 2

1 Ответ

0 голосов
/ 28 декабря 2018

Предполагая, что 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 для проведения теста до его завершения.

Существуют и другие зависимости, которые контроллер должен обеспечить, чтобы тест проходил, но в настоящее время он выходит за рамки вопроса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...