繁体   English   中英

如何单元测试 Core MVC controller 操作是否调用 ControllerBase.Problem()

[英]How to unit test whether a Core MVC controller action calls ControllerBase.Problem()

我们有一个 controller 派生自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()抛出 Null 引用异常。 这是 Core MVC 框架的一个方法,所以我真的不知道它为什么会抛出错误。 我想可能是因为 HttpContext 是 null,但我不确定。 是否有标准化的方法来测试 controller 应该调用Problem()的测试用例? 任何帮助表示赞赏。 如果答案涉及 mocking:我们使用 Moq 和 AutoFixtrue。

null 异常是因为缺少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
    };
}

来源

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;
    }
}

来源

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

[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
}

我是通过相关问题来回答这个问题的: 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去掉那里的选项参数。 然后只需在单元测试的控制器中设置它的一个实例,并按预期查看返回的对象!

为了改进EricBDev 的答案(避免在测试中创建ProblemsDetailFactory的任何实现)和Nkosi 的答案(允许验证创建Problem时使用的值),您可以模拟ProblemsDetailFactory以返回一个空的ProblemsDetail (以避免 NRE)然后验证对模拟工厂的调用,以确保被测代码将正确的状态代码、详细信息等传递给它。

示例:(使用Moq

// create the mock `ProblemDetailsFactory`
var problemDetailsFactoryMock = new Mock<ProblemDetailsFactory>();
// set it up to return an empty `Problems` object (to avoid the `NullReferenceException`s)
problemDetailsFactoryMock.Setup(p =>
    p.CreateProblemDetails(
        It.IsAny<HttpContext>(),
        It.IsAny<int>(),     // statusCode
        It.IsAny<string>(),  // title
        It.IsAny<string>(),  // type
        It.IsAny<string>(),  // detail
        It.IsAny<string>())  // instance
    ).Returns(new ProblemDetails());

// your other test code here

// verify the arguments passed to `Problem(...)`
_problemDetailsFactoryMock.Verify(p =>
    p.CreateProblemDetails(
        It.IsAny<HttpContext>(),
        (int)HttpStatusCode.Forbidden,  // or whatever StatusCode you expect
        default,                        // or whatever you expect for `Title`
        default,                        // or whatever you expect for `Type`
        It.Is<string>(s => s.Contains("whatever you expect in the Detail", StringComparison.OrdinalIgnoreCase)),
        default                         // or whatever you expect for `Instance`
    ));

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

...
MyController controller;

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

暂无
暂无

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

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