0

I'm writing unit test for user creation method in my controller. When I run unit test it returns NullReferenceException in the line return ValidationProblem(); in my controller method.

[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 ---

My controller method

        [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();
            }

        }

Unit test

        [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);
        }
kubwlo
  • 87
  • 2
  • 6

4 Answers4

5

Look at the source of the method that throws:

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(...);

That looks like it can throw. Then where does 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;
    }
}

You didn't provide an HttpContext to your controller (and if you did, you didn't register a ProblemDetailsFactory), so indeed, this getter returns null, causing the call to CreateValidationProblemDetails() to throw a NRE.

So you need to provide it. The DefaultProblemDetailsFactory that ASP.NET uses is internal, so you better mock it:

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

And then set up the call you expect.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
4

If I had to guess, I'd say ControllerBase.ValidationProblem probably tries to access the HTTP context, which is not available while unit testing. You'll have to mock the HTTP context, like so: https://stackoverflow.com/a/2497618/1185136

Rudey
  • 4,717
  • 4
  • 42
  • 84
2

As @Rudery said and if take a look at the ValidationProblem implementation, you have to mock HttpContext because ProblemDetailsFactory.CreateValidationProblemDetails needs that to create validationProblem object:

[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

If you take a look at the ASP.NET Core test for ValidationProblem you found out that you need to mock 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

Mohsen Esmailpour
  • 11,224
  • 3
  • 45
  • 66
  • _"have to mock HttpContext"_ - and its `RequestServices`, and register a `ProblemDetailsFactory`, and so on. Don't just mock HttpContext to provide in dependency injection. – CodeCaster Aug 14 '20 at 13:24
  • _"You can use ASP.NET Core test sample for ValidationProblem to setup required objects"_ - no, they can't, `DefaultProblemDetailsFactory` is internal (and visible to the test projects, but not OP's test project). – CodeCaster Aug 14 '20 at 13:33
  • There is no need to use `DefaultProblemDetailsFactory`, just a mock is needed. – Mohsen Esmailpour Aug 14 '20 at 13:40
0

With your help I've got this to work by mocking ProblemDetailsDactory, CreateValidationProblemDetails method and HttpContext. Thank you.


            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;
kubwlo
  • 87
  • 2
  • 6