【问题标题】:Unit testing a controller action which calls a private method that uses HTTPClient对调用使用 HTTPClient 的私有方法的控制器操作进行单元测试
【发布时间】:2020-04-27 08:54:14
【问题描述】:

我是 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.应该调用token provider服务并获取token 2.使用token,验证服务添加实体

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 方法时失败,因为它未能将 GetAccessTokenDetailsAsync() 私有方法中的 responseStream.Result 反序列化为此单元测试中的 AccessTokenDetails。反序列化失败,因为 responseStream.Result 的值只是一个 GUID 字符串。

谁能告诉我我遇到了“依赖倒置”问题并告诉我解决这个问题的方法吗?

我正在考虑将 GetAccessTokenDetailsAsync 分离到不同的类,例如 AccessTokenProvider 并模拟它来克服它 - 这是一个好方法吗?解决这个问题的最佳方法是什么。

【问题讨论】:

  • 两件事 - 1)如果您不提供模拟服务器作为令牌源,我相信您没有删除足够的依赖项来进行单元测试,所以它可能是一个行为测试。 2)如果您的函数由于服务器返回不兼容的数据结构而失败,那么我建议测试 Fail 已经达到了它的目的并且很有帮助 - 现在要么验证服务器返回错误的信息并修复它,要么重写你的反序列化以根据返回的 guid 生成您想要的结构(如果可能的话)。

标签: c# unit-testing moq


【解决方案1】:

好的,让我们把一些事情弄清楚。

  1. 并非所有内容都应该进行单元测试。您有一个 API,并且您依赖于令牌服务。这两件事需要进行集成测试。模拟和调用 API 方法不会给您任何价值。 单元测试业务功能。从你开始谈论模拟控制器的那一刻起,你就走上了一条没有真正目的的道路。您需要将业务功能与控制器分离

  2. 你没有做 TDD。 TDD 意味着你从失败的测试开始,你做的第一件事是编写测试,然后开始编写代码来满足这些测试。如果您从一开始就这样做,那么您现在发现的所有这些问题都已经解决了。

  3. 了解如何正确调用 API。您提到使用 responseStream.Result 。这是不知道如何正确使用异步的人的标志。您需要正确等待您的电话。

这是一个基于快速搜索的示例:How do I correctly use HttpClient with async/await?

注意。 Http 客户端不应该在 using 块内使用,这实际上会适得其反。过一遍这个,例如:https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

  1. 如果您想进行适当的单元测试,请停止考虑控制器并开始考虑功能。如果您的代码正确分离,则无需模拟控制器。您可以在 API 之外简单地对这些单独的类/库进行单元测试。

  2. 如果您想确定您的 API 确实有效,请停止模拟调用。对它进行真正的调用,计划您的输入并检查输出。这就是为什么我说你集成测试端点。

同样适用于令牌端点。使用真实通话,获取真实代币,看看出现问题时会发生什么。

【讨论】:

  • 感谢您的建议。我同意调用 .Result 是一个错误,我应该等待。我不认为我没有在 using 块中使用 HttpClient,但我正在使用 HttpClientFactory。我需要提高代码覆盖率——这就是我需要对这个方法进行单元测试的原因。您是否知道在不进行单元测试或在覆盖率扫描仪中排除 .cs 文件的情况下提高代码覆盖率?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-19
  • 2018-06-20
相关资源
最近更新 更多