【问题标题】:.Net Core NullReferenceException when using ControllerBase.ValidationProblem().Net Core NullReferenceException 使用 ControllerBase.ValidationProblem()
【发布时间】:2020-08-14 13:04:24
【问题描述】:

我正在为我的控制器中的用户创建方法编写单元测试。 当我运行单元测试时,它会在行中返回 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);
        }

【问题讨论】:

    标签: c# asp.net-core moq xunit


    【解决方案1】:

    查看抛出的方法的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返回null,导致对CreateValidationProblemDetails()的调用扔一个 NRE。

    所以你需要提供它。 ASP.NET 使用的 DefaultProblemDetailsFactory 是internal,所以你最好模拟一下:

    controller.ProblemDetailsFactory = new Mock<ProblemDetailsFactory>();
    

    然后设置您期望的呼叫。

    【讨论】:

    • 谢谢。模拟 ProblemDetailsFactory 确实有问题。
    【解决方案2】:

    如果我不得不猜测,我会说ControllerBase.ValidationProblem 可能会尝试访问 HTTP 上下文,这在单元测试时不可用。您必须模拟 HTTP 上下文,如下所示:https://stackoverflow.com/a/2497618/1185136

    【讨论】:

      【解决方案3】:

      正如@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 Core 测试,您会发现您需要模拟 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

      【讨论】:

      • "必须模拟 HttpContext" - 及其RequestServices,并注册一个ProblemDetailsFactory,等等。不要只是模拟 HttpContext 来提供依赖注入。
      • “您可以使用 ValidationProblem 的 ASP.NET Core 测试示例来设置所需的对象” - 不,他们不能,DefaultProblemDetailsFactory 是内部的(并且对测试项目,但不是 OP 的测试项目)。
      • 不用DefaultProblemDetailsFactory,只需要一个mock。
      【解决方案4】:

      在您的帮助下,我通过模拟 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;
      

      【讨论】:

        猜你喜欢
        • 2019-07-24
        • 1970-01-01
        • 2017-12-30
        • 2018-04-02
        • 1970-01-01
        • 1970-01-01
        • 2018-07-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多