C# 异步任务的单元测试<;行动结果>;

C# 异步任务的单元测试<;行动结果>;,c#,unit-testing,asp.net-core-mvc,C#,Unit Testing,Asp.net Core Mvc,我对ASP.NET核心MVC非常陌生,我想知道是否有人能帮我解决我的问题 我在一个项目中工作,该项目将获得特定azure devops组织中的所有项目 这是我的控制器代码: public async Task<ActionResult> Organization(string selectedOrg, string oauth) { var client = new HttpClient(); IndexViewModel model = new IndexViewM

我对ASP.NET核心MVC非常陌生,我想知道是否有人能帮我解决我的问题

我在一个项目中工作,该项目将获得特定azure devops组织中的所有项目

这是我的控制器代码:

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()));
    }
}
公共异步任务组织(字符串selectedOrg,字符串oauth)
{
var client=新的HttpClient();
IndexViewModel model=新的IndexViewModel();
model.Organizations=OrganizationData.Data;
if(selectedOrg==null)
{
选择org=model.Organizations.FirstOrDefault().OrgName;
}
其他的
{
model.SelectedOrg=SelectedOrg;
}
var token=_cache.Get(“token”+HttpContext.Session.GetString(“TokenGuid”);
oauth=token.AccessToken;
变量url=”https://dev.azure.com/“+selectedOrg+”/_api/projects?api版本=4.1”;
尝试
{
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Authorization=新的AuthenticationHeaderValue(“承载者”,oauth);
var response=wait client.GetAsync(url);
var responseBody=response.Content.ReadAsStringAsync().Result;
model.Projects=JsonConvert.DeserializeObject(responseBody);
client.Dispose();
返回视图(“索引”,模型);
}
捕获(例外e)
{
client.Dispose();
返回Json(Newtonsoft.Json.JsonConvert.DeserializeObject(e.ToString());
}
}

有谁能帮我用这个做单元测试吗?还是我必须重构这个

对于许多依赖项,您有方法

为什么方法的签名传递一个从未使用过的
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;
   }
  }
}
现在谈谈最难的部分:

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()));
    }
}
公共异步任务组织(字符串selectedOrg,字符串oauth)
{
IndexViewModel model=新的IndexViewModel();
//我不知道模特是从哪里来的
//这似乎阻止了“单元测试”
//奇怪的是,你没有验证'selectedOrg',你只是使用它
model.Organizations=OrganizationData.Data;
if(selectedOrg==null)
{
选择org=model.Organizations.FirstOrDefault().OrgName;
}
其他的
{
model.SelectedOrg=SelectedOrg;
}
//不知道`_cache`是从哪里来的
//也似乎阻止了“单元测试”
//“HttpContext”也是如此,因为您没有使用
//接口
var token=_cache.Get(“token”+HttpContext.Session.GetString(“TokenGuid”);
oauth=token.AccessToken;
尝试
{
var orgInfo=await _azureAPI.getorgancy(selectedOrg,oauth);
model.Projects=JsonConvert.DeserializeObject(orgInfo);
//在这里返回视图???
返回视图(“索引”,模型);
}
捕获(例外e)
{
//在这里返回JSON吗????
返回Json(Newtonsoft.Json.JsonConvert.DeserializeObject(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
  {
    //...
  }

}
公共MyControllerTests
{
//对于100%的保险范围,您需要所有这些
公共异步任务组织\u OrgaString\u ReturnsView
{
//...
}
公共异步任务组织\u OrgAsNull\u ReturnsView
{
//安排
var azureAPI=替换为();
azureAPI.GetOrgAsync(null,null)
.Returns(“somestring”);
var控制器=新的MyController(azureAPI);
//表演
var result=await controller.Organization(null,null);
//断言
断言(结果…);
}
公共异步任务组织\u带异常\u返回
{
//...
}
}

但是,该代码与实现问题紧密耦合,不允许单独进行单元测试。理想情况下,您希望能够模拟/存根所有依赖项,并将它们注入测试主题。感谢您对这一点的评论。是否可以仅针对json结果进行单元测试?您需要提取http调用,以便伪造json响应。我还意识到您将阻塞
.result
与异步等待混合在一起,这可能会导致死锁。这很难回答,因为有许多问题需要考虑。首先,更改
response.Content.ReadAsStringAsync().Result
to
wait response.Content.ReadAsStringAsync()
解决@nkosi提到的
.Result
问题。然后使用您最喜欢的搜索引擎了解为什么创建这样的
HttpClient
s是个坏主意,以及您可能希望如何使用依赖项注入。最后,通过抽象,@nkosi可能意味着创建另一个类来处理对Azure端点的请求,并将其注入控制器中。使用Nsubstitute,异步方法不需要显式返回
Task
azureAPI.GetOrgAsync(null,null)。返回(“somestring”)
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
  {
    //...
  }

}