【问题标题】:How to unit test whether a Core MVC controller action calls ControllerBase.Problem()如何对 Core MVC 控制器操作是否调用 ControllerBase.Problem() 进行单元测试
【发布时间】:2020-03-18 12:59:52
【问题描述】:

我们有一个从ControllerBase 派生的控制器,其操作如下:

public async Task<ActionResult> Get(int id)
{
  try
  {
    // Logic
    return Ok(someReturnValue);
  }
  catch
  {
    return Problem();
  }
}

我们也有这样的单元测试:

[TestMethod]
public async Task GetCallsProblemOnInvalidId()
{
  var result = sut.Get(someInvalidId);

}

但是ControllerBase.Problem() 会抛出空引用异常。这是来自 Core MVC 框架的方法,所以我真的不知道它为什么会抛出错误。我认为可能是因为 HttpContext 为空,但我不确定。 是否有一种标准化的方法来测试控制器应该调用Problem() 的测试用例? 任何帮助表示赞赏。 如果答案涉及模拟:我们使用 Moq 和 AutoFixtrue。

【问题讨论】:

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


    【解决方案1】:

    空异常是因为缺少ProblemDetailsFactory

    在这种情况下,控制器需要能够通过以下方式创建ProblemDetails 实例

    [NonAction]
    public virtual ObjectResult Problem(
        string detail = null,
        string instance = null,
        int? statusCode = null,
        string title = null,
        string type = null)
    {
        var problemDetails = ProblemDetailsFactory.CreateProblemDetails(
            HttpContext,
            statusCode: statusCode ?? 500,
            title: title,
            type: type,
            detail: detail,
            instance: instance);
    
        return new ObjectResult(problemDetails)
        {
            StatusCode = problemDetails.Status
        };
    }
    

    Source

    ProblemDetailsFactory 是一个可设置的属性

    public ProblemDetailsFactory ProblemDetailsFactory
    {
        get
        {
            if (_problemDetailsFactory == null)
            {
                _problemDetailsFactory = HttpContext?.RequestServices?.GetRequiredService<ProblemDetailsFactory>();
            }
    
            return _problemDetailsFactory;
        }
        set
        {
            if (value == null)
            {
                throw new ArgumentNullException(nameof(value));
            }
    
            _problemDetailsFactory = value;
        }
    }
    

    Source

    在单独测试时可以模拟和填充。

    [TestMethod]
    public async Task GetCallsProblemOnInvalidId() {
        //Arrange
        var problemDetails = new ProblemDetails() {
            //...populate as needed
        };
        var mock = new Mock<ProblemDetailsFactory>();
        mock
            .Setup(_ => _.CreateProblemDetails(
                It.IsAny<HttpContext>(),
                It.IsAny<int?>(),
                It.IsAny<string>(),
                It.IsAny<string>(),
                It.IsAny<string>(),
                It.IsAny<string>())
            )
            .Returns(problemDetails)
            .Verifyable();
    
        var sut = new MyController(...);
        sut.ProblemDetailsFactory = mock.Object;
    
        //...
    
        //Act
        var result = await sut.Get(someInvalidId);
    
        //Assert
        mock.Verify();//verify setup(s) invoked as expected
    
        //...other assertions
    }
    

    【讨论】:

      【解决方案2】:

      我通过相关问题提出了这个问题: https://github.com/dotnet/aspnetcore/issues/15166

      Nkosi 正确地指向了后台 ProblemDetailsFactory。

      请注意,该问题已在 .NET 5.x 中修复,但在 LTS .NET 3.1.x 中未修复,正如您在 Nkosi 引用的源代码中看到的(通过在 Github 中切换分支/标签)

      正如 Nkosi 所说,诀窍是在单元测试中设置控制器的 ProblemDetailsFactory 属性。 Nkosi 建议模拟 ProblemDetailsFactory,但是按照上面的操作,您无法在单元测试中验证 Problem 对象的值。 另一种方法是简单地设置 ProblemDetailsFactory 的实际实现,例如将 DefaultProblemDetailsFactory 从 Microsoft(内部类)复制到您的 UnitTest 项目: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs 摆脱那里的选项参数。 然后只需在单元测试的控制器中设置它的一个实例,并按预期查看返回的对象!

      【讨论】:

        【解决方案3】:

        在您的测试中,如果您首先创建一个 ControllerContext,那么在执行控制器代码时应该会按预期创建 ProblemDetails。

        ...
        MyController controller;
        
        [Setup]
        public void Setup()
        {
            controller = new MyController();
            controller.ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    // add other mocks or fakes 
                }
            };
        }
        ...
        

        【讨论】:

        • 根据 /// 文档 HttpContext “封装有关单个 HTTP 请求的所有 HTTP 特定信息。”。这对提供 ProblemDetailsFactory 有何帮助?对我来说,这并没有解决这个问题。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-27
        • 2010-11-26
        • 1970-01-01
        • 1970-01-01
        • 2011-08-12
        • 1970-01-01
        相关资源
        最近更新 更多