使用 ControllerBase.ValidationProblem() 时出现 .Net Core NullReferenceException
.Net Core NullReferenceException when using ControllerBase.ValidationProblem()
我正在为控制器中的用户创建方法编写单元测试。
当我 运行 对其进行单元测试时 returns NullReferenceException 行
return ValidationProblem();
在我的控制器方法中。
[xUnit.net 00:00:01.16] WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData [FAIL]
X WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData [285ms]
Error Message:
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(String detail, String instance, Nullable`1 statusCode, String title, String type, ModelStateDictionary modelStateDictionary)
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(ModelStateDictionary modelStateDictionary)
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem()
at WorkTimeManager.Controllers.UsersController.Post(UserCreateDto user) in /mnt/c/Users/kubw1/WorkTimeManagerSolution/src/WorkTimeManager/Controllers/UsersController.cs:line 72
at WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData() in /mnt/c/Users/kubw1/WorkTimeManagerSolution/test/WotkTimeManager.Tests/UsersControllerTests.cs:line 92
--- End of stack trace from previous location where exception was thrown ---
我的控制器方法
[HttpPost]
public async Task<ActionResult<string>> Post(UserCreateDto user)
{
var userModel = _mapper.Map<User>(user);
var result = await _userManager.CreateAsync(userModel, user.password);
if (result.Succeeded)
{
return Ok();
}
else
{
foreach (var err in result.Errors)
{
ModelState.AddModelError(err.Code, err.Description);
}
return ValidationProblem();
}
}
单元测试
[Fact]
public async Task PostUsers_BadResult_WhenInvalidData()
{
var user = new UserCreateDto
{
username = "test",
password = "testp",
email = "email@wp.pl"
};
userManager
.Setup(x => x.CreateAsync(It.IsAny<User>(), It.IsAny<string>()))
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "Problem", Description = "Not working" })).Verifiable();
controller = new UsersController(new UnitOfWork(dbContext), userManager.Object, mapper);
var result = await controller.Post(user);
Assert.IsType<ValidationProblemDetails>(result.Result);
}
如果非要我猜的话,我会说 ControllerBase.ValidationProblem
可能会尝试访问 HTTP 上下文,这在单元测试时不可用。您必须像这样模拟 HTTP 上下文:
正如@Rudery 所说,如果看一下 ValidationProblem
实现,您必须模拟 HttpContext
因为 ProblemDetailsFactory.CreateValidationProblemDetails
需要它来创建 validationProblem
对象:
[NonAction]
public virtual ActionResult ValidationProblem(
string detail = null,
string instance = null,
int? statusCode = null,
string title = null,
string type = null,
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
{
modelStateDictionary ??= ModelState;
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(
HttpContext,
modelStateDictionary,
statusCode: statusCode,
title: title,
type: type,
detail: detail,
instance: instance);
...
如果您查看 ValidationProblem
的 ASP.NET 核心测试,您会发现您需要模拟 ProblemDetailsFactory
[Fact]
public void ValidationProblemDetails_Works()
{
// Arrange
var context = new ControllerContext(new ActionContext(
new DefaultHttpContext { TraceIdentifier = "some-trace" },
new RouteData(),
new ControllerActionDescriptor()));
context.ModelState.AddModelError("key1", "error1");
var controller = new TestableController
{
ProblemDetailsFactory = // Mock ProblemDetailsFactory
ControllerContext = context,
};
...
查看抛出方法的source:
public virtual ActionResult ValidationProblem(
string detail = null,
string instance = null,
int? statusCode = null,
string title = null,
string type = null,
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
{
modelStateDictionary ??= ModelState;
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(...);
看起来它可以扔。那么ProblemDetailsFactory
come from呢?
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;
}
}
您没有向您的控制器提供 HttpContext
(即使您提供了,您也没有注册 ProblemDetailsFactory
),所以确实如此 getter returns null
,导致对 CreateValidationProblemDetails()
的调用抛出 NRE。
所以你需要提供。 ASP.NET 使用的 DefaultProblemDetailsFactory 是 internal
,所以你最好模拟它:
controller.ProblemDetailsFactory = new Mock<ProblemDetailsFactory>();
然后设置您期望的呼叫。
在您的帮助下,我通过模拟 ProblemDetailsDactory、CreateValidationProblemDetails 方法和 HttpContext 使其工作。
谢谢。
controller = new UsersController(new UnitOfWork(dbContext), userManager.Object, mapper);
var ctx = new ControllerContext() { HttpContext = new DefaultHttpContext() };
controller.ControllerContext = ctx;
var problemDetails = new ValidationProblemDetails();
var mock = new Mock<ProblemDetailsFactory>();
mock
.Setup(_ => _.CreateValidationProblemDetails(
It.IsAny<HttpContext>(),
It.IsAny<ModelStateDictionary>(),
It.IsAny<int?>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>())
)
.Returns(problemDetails);
controller.ProblemDetailsFactory = mock.Object;
我正在为控制器中的用户创建方法编写单元测试。
当我 运行 对其进行单元测试时 returns NullReferenceException 行
return ValidationProblem();
在我的控制器方法中。
[xUnit.net 00:00:01.16] WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData [FAIL]
X WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData [285ms]
Error Message:
System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(String detail, String instance, Nullable`1 statusCode, String title, String type, ModelStateDictionary modelStateDictionary)
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem(ModelStateDictionary modelStateDictionary)
at Microsoft.AspNetCore.Mvc.ControllerBase.ValidationProblem()
at WorkTimeManager.Controllers.UsersController.Post(UserCreateDto user) in /mnt/c/Users/kubw1/WorkTimeManagerSolution/src/WorkTimeManager/Controllers/UsersController.cs:line 72
at WotkTimeManager.Tests.UsersControllerTests.PostUsers_BadResult_WhenInvalidData() in /mnt/c/Users/kubw1/WorkTimeManagerSolution/test/WotkTimeManager.Tests/UsersControllerTests.cs:line 92
--- End of stack trace from previous location where exception was thrown ---
我的控制器方法
[HttpPost]
public async Task<ActionResult<string>> Post(UserCreateDto user)
{
var userModel = _mapper.Map<User>(user);
var result = await _userManager.CreateAsync(userModel, user.password);
if (result.Succeeded)
{
return Ok();
}
else
{
foreach (var err in result.Errors)
{
ModelState.AddModelError(err.Code, err.Description);
}
return ValidationProblem();
}
}
单元测试
[Fact]
public async Task PostUsers_BadResult_WhenInvalidData()
{
var user = new UserCreateDto
{
username = "test",
password = "testp",
email = "email@wp.pl"
};
userManager
.Setup(x => x.CreateAsync(It.IsAny<User>(), It.IsAny<string>()))
.ReturnsAsync(IdentityResult.Failed(new IdentityError { Code = "Problem", Description = "Not working" })).Verifiable();
controller = new UsersController(new UnitOfWork(dbContext), userManager.Object, mapper);
var result = await controller.Post(user);
Assert.IsType<ValidationProblemDetails>(result.Result);
}
如果非要我猜的话,我会说 ControllerBase.ValidationProblem
可能会尝试访问 HTTP 上下文,这在单元测试时不可用。您必须像这样模拟 HTTP 上下文:
正如@Rudery 所说,如果看一下 ValidationProblem
实现,您必须模拟 HttpContext
因为 ProblemDetailsFactory.CreateValidationProblemDetails
需要它来创建 validationProblem
对象:
[NonAction]
public virtual ActionResult ValidationProblem(
string detail = null,
string instance = null,
int? statusCode = null,
string title = null,
string type = null,
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
{
modelStateDictionary ??= ModelState;
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(
HttpContext,
modelStateDictionary,
statusCode: statusCode,
title: title,
type: type,
detail: detail,
instance: instance);
...
如果您查看 ValidationProblem
的 ASP.NET 核心测试,您会发现您需要模拟 ProblemDetailsFactory
[Fact]
public void ValidationProblemDetails_Works()
{
// Arrange
var context = new ControllerContext(new ActionContext(
new DefaultHttpContext { TraceIdentifier = "some-trace" },
new RouteData(),
new ControllerActionDescriptor()));
context.ModelState.AddModelError("key1", "error1");
var controller = new TestableController
{
ProblemDetailsFactory = // Mock ProblemDetailsFactory
ControllerContext = context,
};
...
查看抛出方法的source:
public virtual ActionResult ValidationProblem(
string detail = null,
string instance = null,
int? statusCode = null,
string title = null,
string type = null,
[ActionResultObjectValue] ModelStateDictionary modelStateDictionary = null)
{
modelStateDictionary ??= ModelState;
var validationProblem = ProblemDetailsFactory.CreateValidationProblemDetails(...);
看起来它可以扔。那么ProblemDetailsFactory
come from呢?
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;
}
}
您没有向您的控制器提供 HttpContext
(即使您提供了,您也没有注册 ProblemDetailsFactory
),所以确实如此 getter returns null
,导致对 CreateValidationProblemDetails()
的调用抛出 NRE。
所以你需要提供。 ASP.NET 使用的 DefaultProblemDetailsFactory 是 internal
,所以你最好模拟它:
controller.ProblemDetailsFactory = new Mock<ProblemDetailsFactory>();
然后设置您期望的呼叫。
在您的帮助下,我通过模拟 ProblemDetailsDactory、CreateValidationProblemDetails 方法和 HttpContext 使其工作。 谢谢。
controller = new UsersController(new UnitOfWork(dbContext), userManager.Object, mapper);
var ctx = new ControllerContext() { HttpContext = new DefaultHttpContext() };
controller.ControllerContext = ctx;
var problemDetails = new ValidationProblemDetails();
var mock = new Mock<ProblemDetailsFactory>();
mock
.Setup(_ => _.CreateValidationProblemDetails(
It.IsAny<HttpContext>(),
It.IsAny<ModelStateDictionary>(),
It.IsAny<int?>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<string>())
)
.Returns(problemDetails);
controller.ProblemDetailsFactory = mock.Object;