Модульное тестирование действия контроллера, которое вызывает закрытый метод, который использует HTTPClient - PullRequest
0 голосов
/ 27 апреля 2020

Я новичок ie до C# и TDD. Я разрабатываю продукт, в котором мне нужно написать модульные тесты для некоторых вызовов HTTP API. Ниже показано, как выглядит контроллер:

public class CommunicationController : ControllerBase
{
 private readonly IHttpClientFactory _clientFactory;
 private readonly AppSettings _appSettings;

 public CommunicationController(IHttpClientFactory clientFactory, IOptions<AppSettings> appSettings)
 {
  _clientFactory = clientFactory;
  _appSettings = appSettings.Value;
 }

 [HttpPost]
 public async Task<IActionResult> PostEntity([FromBody] Entity entity)
 {
  if (entity.foo == null)
  {
   NoActionsMessage noActionsMessage = new NoActionsMessage
   {
    Message = "No actions performed"
   };
   return Ok(noActionsMessage);
  }

  var accessTokenDatails = await GetAccessTokenDetailsAsync();
  var callUrl = "http://someUrlGoesHere";

  var json = JsonConvert.SerializeObject(entity);
  var content = new System.Net.Http.StringContent(json, Encoding.UTF8, "application/json");
  var request = new HttpRequestMessage(HttpMethod.Put, new Uri(callUrl))
  {
   Content = content
  };
  request.Headers.Add("accessToken", accessTokenDatails.AccessToken);

  return await InvokeHttpCall(request);
 }


 private async Task<AccessTokenDetails> GetAccessTokenDetailsAsync()
 {
  var appId = _appSettings.AppId;
  var appSecret = _appSettings.AppSecret;
  var refreshToken = _appSettings.RefreshToken;

  var request = new HttpRequestMessage(HttpMethod.Get, new Uri("sometokenproviderUrl"));

  request.Headers.Add("applicationId", appId);
  request.Headers.Add("applicationSecret", appSecret);
  request.Headers.Add("refreshToken", refreshToken);

  var client = _clientFactory.CreateClient();

  var response = await client.SendAsync(request);

  if (response.IsSuccessStatusCode)
  {
   var responseStream = response.Content.ReadAsStringAsync();

   // [ALERT] the failing line in unit test - because responseStream.Result is just a GUID and this the the problem
   var result = JsonConvert.DeserializeObject<AccessTokenDetails>(responseStream.Result);

   return result;
  }
  else
  {
   throw new ArgumentException("Unable to get  Access Token");
  }
 }
}

Этот метод POST вызывает частный метод. Вызывая этот метод post с соответствующей сущностью, переданной: 1. Должен позвонить в службу провайдера токена и получить токен 2. С помощью токена аутентифицируйте службу, чтобы добавить сущность

AccessTokenDetails класс выглядит ниже:

public sealed class AccessTokenDetails
{
 [JsonProperty("accessToken")]
 public string AccessToken { get; set; }

 [JsonProperty("endpointUrl")]
 public Uri EndpointUrl { get; set; }

 [JsonProperty("accessTokenExpiry")]
 public long AccessTokenExpiry { get; set; }

 [JsonProperty("scope")]
 public string Scope { get; set; }
}

Теперь, когда речь идет о модульном тестировании (я использую XUnit), у меня есть метод тестирования, подобный приведенному ниже:

public async Task Entity_Post_Should_Return_OK()
{
 / Arrange - IHttpClientFactoryHttpClientFactory
 var httpClientFactory = new Mock<IHttpClientFactory>();
 var mockHttpMessageHandler = new Mock<HttpMessageHandler>();
 var fixture = new Fixture();

 mockHttpMessageHandler.Protected()
  .Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
  .ReturnsAsync(new HttpResponseMessage
  {
   StatusCode = HttpStatusCode.OK,
   Content = new StringContent(fixture.Create<string>),
  });

 var client = new HttpClient(mockHttpMessageHandler.Object);
 client.BaseAddress = fixture.Create<Uri>();
 httpClientFactory.Setup(_ => _.CreateClient(It.IsAny<string>())).Returns(client);

 // Arrange - IOptions
 var optionsMock = new Mock<IOptions<AppSettings>>();
 optionsMock.SetupGet(o => o.Value).Returns(new AppSettings
 {
  AppId = "mockappid",
  AppSecret = "mockappsecret",
  RefreshToken = "mockrefreshtoken"
 });

 // Arrange - Entity
 AddActionEntity entity = new Entity();
 entity.foo = "justfoo";

 // Act  
 var controller = new CommunicationController(httpClientFactory.Object, optionsMock.Object);
 var result = await controller.PostEntity(entity);

 // Assert  
 Assert.NotNull(result);
 Assert.IsAssignableFrom<OkObjectResult>(result);
}

Этот конкретный тестовый пример не срабатывает, когда вызов метода PostEntity, так как не удалось десериализовать responseStream.Result в приватном методе GetAccessTokenDetailsAsync(), в AccessTokenDetails в этом модульном тесте. Десериализация не удалась, так как значение responseStream.Result является просто строкой GUID.

Может кто-нибудь сказать мне, что я сталкиваюсь с проблемой "инверсии зависимости", и предложить мне способ ее преодоления?

Я думаю о том, чтобы отделить GetAccessTokenDetailsAsync от другого класса, что-то вроде AccessTokenProvider, и высмеивать его, чтобы это произошло - будет ли это хорошим подходом? что может быть лучшим подходом для решения этой проблемы.

1 Ответ

0 голосов
/ 27 апреля 2020

хорошо, давайте разберемся несколько вещей.

  1. не все должно быть проверено модулем. У вас есть API, и у вас есть зависимость от службы токенов. Эти две вещи должны быть проверены на интеграцию. Насмешливые и вызывающие методы API не принесут вам никакой пользы. Модульный тест бизнес-функциональности. В тот момент, когда вы начинаете говорить о насмешливых контроллерах, вы идете по пути, который не преследует никакой реальной цели. Вам необходимо отделить функциональность вашего бизнеса от контроллеров

  2. Вы не делаете TDD. TDD означает, что вы начинаете с неудачных тестов, в первую очередь вы пишете тесты, а затем начинаете писать код, удовлетворяющий этим тестам. Если бы вы сделали это с самого начала, все проблемы, которые вы обнаружили сейчас, были бы уже решены.

  3. Узнайте, как правильно вызывать API. Вы упоминаете, используя responseStream.Result. Это признак того, кто не знает, как правильно использовать asyn c. Вам нужно правильно ожидать ваших звонков.

Вот пример, основанный на быстром поиске: Как правильно использовать HttpClient с async / await?

NB. Http-клиент не должен использоваться внутри блока using, это на самом деле контрпродуктивно. Go, например: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

если вы хотите провести правильное модульное тестирование, то перестаньте думать о контроллерах и начните думать о функциональности. Вам не нужно издеваться над контроллером, если ваш код правильно разделен. Вы можете просто выполнить модульное тестирование этих отдельных классов / библиотек вне вашего API.

, если вы хотите быть уверенными в том, что ваш API действительно работает, прекратите насмешливые вызовы. Делайте реальные звонки, планируйте свои входы и проверяйте выходы. Вот почему я сказал, что вы конечные точки тестирования интеграции.

То же самое относится к конечным точкам токена. Используйте реальные звонки, получайте реальные токены и смотрите, что происходит, когда что-то не так go.

...