异步Task <actionresult>的单元测试

时间:2018-10-24 15:12:37

标签: c# unit-testing asp.net-core-mvc

我对ASP.NET CORE MVC还是很陌生,我想知道是否有人可以帮助我解决我的问题。

我当时在一个项目中工作,该项目将使所有项目都归入特定的Azure组织。

这是我的控制器代码:

public async Task<ActionResult> Organization(string selectedOrg, string oauth)
{
    var client = new HttpClient();
    IndexViewModel model = new IndexViewModel();
    model.Organizations = OrganizationData.Data;
    if (selectedOrg == null)
    {
        selectedOrg = model.Organizations.FirstOrDefault().OrgName;
    }
    else
    {
        model.SelectedOrg = selectedOrg;
    }
    var token = _cache.Get<TokenModel>("Token" + HttpContext.Session.GetString("TokenGuid"));
    oauth = token.AccessToken;
    var url = "https://dev.azure.com/" + selectedOrg + "/_apis/projects?api-version=4.1";
    try
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauth);

        var response = await client.GetAsync(url);
        var responseBody = response.Content.ReadAsStringAsync().Result;
        model.Projects = JsonConvert.DeserializeObject<ProjectsModel>(responseBody);

        client.Dispose();
        return View("Index", model);
    }
    catch(Exception e)
    {
        client.Dispose();
        return Json(Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(e.ToString()));
    }
}

有人可以帮助进行此单元测试吗?还是我必须重构这个?

1 个答案:

答案 0 :(得分:1)

有很多依赖

为什么方法的签名传递一个从未使用过的oauth值?

首先,应该禁止在控制器内部通过http调用任何外部依赖项。这整个事情应该抽象为它自己的调用。由于它似乎正在获取数据,因此它实际上应该在您的数据层。 我认为是很可能超出了单独项目的整个n层方法范围的范围,因此让我们只涵盖单元测试的最低要求。

首先,您需要抽象HttpClient。如果它们在外部(在大多数情况下)进行了任何调用,那么您就无法真正执行单元测试方法,因为这不是单元测试,而是集成测试。

// I don't have a full grasp of your complete eco-system so based on the
// minimal information provided, this would at least get you close
public interface IAzureAPI
{
  public Task<string> GetOrgAsync(string org, string oauth);
}

public class AzureAPI : IDisposable
{
  public async Task<string> GetOrgAsync(string org, string oauth)
  {
    // use *using* not try/catch/finally/dispose
    using (var client = new HttpClient())
    {
      client.DefaultRequestHeaders.Accept.Clear();
      client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", oauth);

      var url = "https://dev.azure.com/" + org+ "/_apis/projects?api-version=4.1";

      var response = await client.GetAsync(url);
      // never use `.Result` unless you absolutely know what you are doing
      // always using async/wait if possible
      var result = await response.Content.ReadAsStringAsync(); 
      return result;
   }
  }
}

希望您正在使用DI框架:

public class MyController
{
  private IAzureAPI _azureAPI;
  public MyController(IAzureAPI azureAPI)
  {
    _azureAPI = azureAPI;
  }
}

现在进入最困难的部分:

public async Task<ActionResult> Organization(string selectedOrg, string oauth)
{
    IndexViewModel model = new IndexViewModel();

    // I have no idea where model came from so
    // this appears to block "unit-testing"
    // strange that you don't validate `selectedOrg`, you just use it
    model.Organizations = OrganizationData.Data;
    if (selectedOrg == null)
    {
        selectedOrg = model.Organizations.FirstOrDefault().OrgName;
    }
    else
    {
        model.SelectedOrg = selectedOrg;
    }

    // no idea where `_cache` came from so 
    // also appears to block "unit-testing"
    // As does `HttpContext` because you aren't using the
    // Interface
    var token = _cache.Get<TokenModel>("Token" + HttpContext.Session.GetString("TokenGuid"));
    oauth = token.AccessToken;

    try
    {
        var orgInfo = await _azureAPI.GetOrgAsync(selectedOrg, oauth);

        model.Projects = JsonConvert.DeserializeObject<ProjectsModel>(orgInfo);

        // return a view here???
        return View("Index", model);
    }
    catch(Exception e)
    {
        // return JSON here instead????
        return Json(Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(e.ToString()));
    }
}

这是一个总的开始,但是有太多的未知数,并且有太多依赖关系无法实际编写真实的单元测试。这是根据您提供的信息进行的快速结构和半测试。

public MyControllerTests
{
  // for 100% Cover Coverage you'd need all of these
  public async Task Organization_OrgAsString_ReturnsView
  {
    //...
  }

  public async Task Organization_OrgAsNull_ReturnsView
  {
    // Arrange
    var azureAPI = Substitute.For<IAzureAPI>();
    azureAPI.GetOrgAsync(null, null)
      .Returns("somestring");
    var controller = new MyController(azureAPI);

    // Act
    var result = await controller.Organization(null, null);

    // Assert
    Assert.That(result....);

  }

  public async Task Organization_WithException_ReturnsJson
  {
    //...
  }

}