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 protected]"
            };

            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);
        }
0

4 Answers 4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. It was indeed problem with mocking ProblemDetailsFactory.
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

Comments

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

3 Comments

"have to mock HttpContext" - and its RequestServices, and register a ProblemDetailsFactory, and so on. Don't just mock HttpContext to provide in dependency injection.
"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).
There is no need to use DefaultProblemDetailsFactory, just a mock is needed.
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;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.