使用 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);

    ...

https://github.com/dotnet/aspnetcore/blob/9d7c3aff96e4bd2af7179fc3ee04e2e4a094c593/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1951

如果您查看 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,
    };
    ...

https://github.com/dotnet/aspnetcore/blob/116799fa709ff003781368b578e4efe2fa32e937/src/Mvc/Mvc.Core/test/ControllerBaseTest.cs#L2296

查看抛出方法的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;