[英]How to unit test whether a Core MVC controller action calls ControllerBase.Problem()
We have a controller that derives from ControllerBase
with an action like this:我们有一个 controller 派生自
ControllerBase
,其操作如下:
public async Task<ActionResult> Get(int id)
{
try
{
// Logic
return Ok(someReturnValue);
}
catch
{
return Problem();
}
}
We also have a unit test like this:我们还有这样的单元测试:
[TestMethod]
public async Task GetCallsProblemOnInvalidId()
{
var result = sut.Get(someInvalidId);
}
But ControllerBase.Problem()
throws a Null Reference Exception.但是
ControllerBase.Problem()
抛出 Null 引用异常。 This is a method from the Core MVC framework, so I don't realy know why it is throwing the error.这是 Core MVC 框架的一个方法,所以我真的不知道它为什么会抛出错误。 I think it may be because HttpContext is null, but I'm not sure.
我想可能是因为 HttpContext 是 null,但我不确定。 Is there a standardized way to test a test case where the controller should call
Problem()
?是否有标准化的方法来测试 controller 应该调用
Problem()
的测试用例? Any help is appreciated.任何帮助表示赞赏。 If the answer involves mocking: we use Moq and AutoFixtrue.
如果答案涉及 mocking:我们使用 Moq 和 AutoFixtrue。
The null exception is because of a missing ProblemDetailsFactory
null 异常是因为缺少
ProblemDetailsFactory
In this case the controller needs to be able to create ProblemDetails
instance via在这种情况下,控制器需要能够通过创建
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
is a settable property 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;
}
}
that could be mocked and populated when testing in isolation.在单独测试时可以模拟和填充。
[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
}
I came to this question via the related issue: https://github.com/dotnet/aspnetcore/issues/15166我是通过相关问题来回答这个问题的: https : //github.com/dotnet/aspnetcore/issues/15166
Nkosi correctly pointed to the background ProblemDetailsFactory. Nkosi 正确地指向了背景 ProblemDetailsFactory。
Note that the issue has been fixed in .NET 5.x but NOT in LTS .NET 3.1.x as you can see in the source code referenced by Nkosi (by switching the branches/tags in Github)请注意,该问题已在 .NET 5.x 中修复,但未在 LTS .NET 3.1.x 中修复,如您在 Nkosi 引用的源代码中所见(通过在 Github 中切换分支/标签)
As Nkosi said, the trick is to set the ProblemDetailsFactory property of your controller in your unit tests.正如 Nkosi 所说,诀窍是在单元测试中设置控制器的 ProblemDetailsFactory 属性。 Nkosi suggested to mock the ProblemDetailsFactory, but doing as above, you can't verify the values of the Problem object in your unit tests.
Nkosi 建议模拟 ProblemDetailsFactory,但是按照上述操作,您无法在单元测试中验证 Problem 对象的值。 An alternative is simply to set a real implementation of the ProblemDetailsFactory, for instance copy the DefaultProblemDetailsFactory from Microsoft (internal class) to your UnitTest projects: https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Infrastructure/DefaultProblemDetailsFactory.cs Get rid of the options parameter there.
另一种方法是设置 ProblemDetailsFactory 的真实实现,例如将 DefaultProblemDetailsFactory 从 Microsoft(内部类)复制到您的 UnitTest 项目: https : //github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc .Core/src/Infrastructure/DefaultProblemDetailsFactory.cs去掉那里的选项参数。 Then just set an instance of it in the controller in your unit test and see the returned object as expected!
然后只需在单元测试的控制器中设置它的一个实例,并按预期查看返回的对象!
To improve upon EricBDev's answer (to avoid having to create any implementation of ProblemsDetailFactory
in your tests) and Nkosi's answer (to allow verifying the values used when creating the Problem
), you can mock the ProblemsDetailFactory
to return an empty ProblemsDetail
(to avoid NRE) and then verify the calls to the mocked factory, to make sure the right status code, details, etc. are passed to it by the code under test.为了改进EricBDev 的答案(避免在测试中创建
ProblemsDetailFactory
的任何实现)和Nkosi 的答案(允许验证创建Problem
时使用的值),您可以模拟ProblemsDetailFactory
以返回一个空的ProblemsDetail
(以避免 NRE)然后验证对模拟工厂的调用,以确保被测代码将正确的状态代码、详细信息等传递给它。
Example: (using Moq
)示例:(使用
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`
));
In your tests, if you first create a ControllerContext, then ProblemDetails should be created as expected while executing controller code.在您的测试中,如果您首先创建一个 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.