如何单元测试 Core MVC 控制器操作是否调用 ControllerBase.Problem()
How to unit test whether a Core MVC controller action calls 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 为空,但我不确定。
是否有标准化的方法来测试控制器应调用 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
}
在您的测试中,如果您首先创建一个 ControllerContext,那么在执行控制器代码时应该按预期创建 ProblemDetails。
...
MyController controller;
[Setup]
public void Setup()
{
controller = new MyController();
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
// add other mocks or fakes
}
};
}
...
我通过相关问题来到这个问题:
https://github.com/dotnet/aspnetcore/issues/15166
Nkosi 正确指向后台 ProblemDetailsFactory。
请注意,该问题已在 .NET 5.x 中得到修复,但在 LTS .NET 3 中未得到修复。1.x 正如您在 Nkosi 引用的源代码中看到的那样(通过切换 branches/tags 在 Github)
正如 Nkosi 所说,诀窍是在单元测试中设置控制器的 ProblemDetailsFactory 属性。
Nkosi 建议模拟 ProblemDetailsFactory,但如上所示,您无法在单元测试中验证 Problem 对象的值。
另一种方法是简单地设置 ProblemDetailsFactory 的实际实现,例如从 Microsoft(内部 class)复制 DefaultProblemDetailsFactory 到您的 UnitTest 项目:
https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs
摆脱那里的选项参数。
然后只需在单元测试的控制器中设置它的一个实例,然后按预期查看返回的对象!
我们有一个派生自 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。
空异常是因为缺少 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
}
在您的测试中,如果您首先创建一个 ControllerContext,那么在执行控制器代码时应该按预期创建 ProblemDetails。
...
MyController controller;
[Setup]
public void Setup()
{
controller = new MyController();
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext
{
// add other mocks or fakes
}
};
}
...
我通过相关问题来到这个问题: https://github.com/dotnet/aspnetcore/issues/15166
Nkosi 正确指向后台 ProblemDetailsFactory。
请注意,该问题已在 .NET 5.x 中得到修复,但在 LTS .NET 3 中未得到修复。1.x 正如您在 Nkosi 引用的源代码中看到的那样(通过切换 branches/tags 在 Github)
正如 Nkosi 所说,诀窍是在单元测试中设置控制器的 ProblemDetailsFactory 属性。 Nkosi 建议模拟 ProblemDetailsFactory,但如上所示,您无法在单元测试中验证 Problem 对象的值。 另一种方法是简单地设置 ProblemDetailsFactory 的实际实现,例如从 Microsoft(内部 class)复制 DefaultProblemDetailsFactory 到您的 UnitTest 项目: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs 摆脱那里的选项参数。 然后只需在单元测试的控制器中设置它的一个实例,然后按预期查看返回的对象!