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



我们有一个从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为null,但我不确定。是否有一种标准化的方法来测试控制器应该调用Problem()的测试用例?感谢您的帮助。如果答案涉及嘲讽:我们使用Moq和AutoFixtrue。

空异常是因为缺少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正确地指出了背景问题细节工厂。

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

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

为了改进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,那么在执行控制器代码时应按预期创建ProbemDetails。

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

最新更新