简体   繁体   English

对调用使用 HTTPClient 的私有方法的 controller 操作进行单元测试

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

I am a newbie to C# and TDD.我是 C# 和 TDD 的新手。 I am developing a product in which I need to write unit tests for some HTTP API calls.我正在开发一个产品,我需要为一些 HTTP API 调用编写单元测试。 Below is how a controller looks like:下面是 controller 的样子:

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.此 POST 方法正在调用私有方法。 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通过使用给定的适当实体调用此 post 方法: 1. 应调用令牌提供者服务并获取令牌 2. 使用令牌,验证服务以添加实体

AccessTokenDetails class looks is below: AccessTokenDetails class 看起来如下:

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:现在谈到单元测试(我使用的是 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);
}

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.这个特定的测试用例在调用PostEntity方法时失败,因为它未能将GetAccessTokenDetailsAsync()私有方法中的responseStream.Result反序列化到此单元测试中的AccessTokenDetails The deserialization failed as the value of responseStream.Result is just a GUID string.反序列化失败,因为responseStream.Result的值只是一个 GUID 字符串。

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?我正在考虑将GetAccessTokenDetailsAsync与不同的 class 分离,例如AccessTokenProvider并模拟它来克服它 - 这是一个好方法吗? 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.您有一个 API 并且您依赖于令牌服务。 Those 2 things need to be integration tested.这两件事需要进行集成测试。 Mocking and calling API methods won't give you any value. Mocking 和调用 API 方法不会给你任何价值。 Unit test business functionality.单元测试业务功能。 The moment you start talking about mocking controllers you're going down on a path that serves no real purpose.当您开始谈论 mocking 控制器时,您就走上了一条没有实际用途的道路。 You need to decouple your business functionality from your controllers您需要将业务功能与控制器分离

  2. You're not doing TDD.你没有做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. TDD 意味着你从失败的测试开始,你做的第一件事就是编写测试,然后开始编写代码来满足这些测试。 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.了解如何正确调用 API。 You mention using responseStream.Result .您提到使用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?这是一个基于快速搜索的示例: 如何正确使用 HttpClient 和 async/await?

NB.注意。 Http client is not supposed to be used inside a using block, that's actually counter productive. Http 客户端不应该在 using 块内使用,这实际上会适得其反。 Go over this, for example: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ Go 过这个,例如: 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.如果您的代码正确分离,则无需模拟 controller。 You can simply unit tests those separate classes / libraries outside of your API.您可以简单地对 API 之外的那些单独的类/库进行单元测试。

  2. if you want the certainty that your API actually works, stop mocking calls.如果您想确定您的 API 确实有效,请停止 mocking 调用。 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.使用真实的电话,获取真实的代币,看看当 go 出错时会发生什么。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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