简体   繁体   中英

Unit testing a controller action which calls a private method that uses HTTPClient

I am a newbie to C# and TDD. I am developing a product in which I need to write unit tests for some HTTP API calls. Below is how a controller looks like:

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");
  }
 }
}

This POST method which is calling a private method. By calling this post method with appropriate entity given: 1. Should make a call to the token provider service and get the token 2. Using the token, authenticate the service to add the entity

AccessTokenDetails class looks is below:

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; }
}

Now when it comes to unit testing (I am using XUnit) I have a test method like below:

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);
}

This particular test case is failing in the when calling the PostEntity method as it failed to deserialize the responseStream.Result in the GetAccessTokenDetailsAsync() private method, to AccessTokenDetails in this unit test. The deserialization failed as the value of responseStream.Result is just a GUID string.

Can anyone please tell me that I am getting into a "dependency inversion" problem and tell me a way to overcome this?

I am thinking of separating the GetAccessTokenDetailsAsync to a different class, something like AccessTokenProvider and mock it to over come it - will it be a good approach? what could be a best approach to solve this problem.

ok,let's get a few things straight.

  1. not everything should be unit tested. You have an API and you have a dependency on a token service. Those 2 things need to be integration tested. Mocking and calling API methods won't give you any value. Unit test business functionality. The moment you start talking about mocking controllers you're going down on a path that serves no real purpose. You need to decouple your business functionality from your controllers

  2. You're not doing TDD. TDD means you're starting with failing tests, the first thing you do is write tests, then start to write code to satisfy those tests. If you had done that from beginning all these issues you uncover now would have been solved already.

  3. Learn how to properly call an API. You mention using responseStream.Result . That's the sign of someone who doesn't know how to use async properly. You need to await your calls properly.

Here's an example based on a quick search: How do I correctly use HttpClient with async/await?

NB. Http client is not supposed to be used inside a using block, that's actually counter productive. Go over this, for example: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

  1. if you want to do proper unit testing, then stop thinking in terms of controllers and start thinking in terms of functionality. You do not need to mock a controller if your code is properly separated. You can simply unit tests those separate classes / libraries outside of your API.

  2. if you want the certainty that your API actually works, stop mocking calls. Make real calls to it, plan your inputs and check the outputs. That's why I said that you integration test endpoints.

Same applies to the token endpoints. Use real calls, get real tokens and see what happens when things go wrong.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM