Search code examples
unit-testingasp.net-coremoqxunit

Unit Testing Controller ActionResult<T> Response


I'm trying to unit test an ASP.NET 5 controller. The controller is returning a value wrapped in an ActionResult.

The ActionResult seems to have 2 properties - Result and Value. The Value is null, but my service response containing the data is inside the Result. It is not as simple as accessing it by actionResult.Result.Value.Data.Id.

Other posts suggest using ((ObjectResult)actual.Result).Value which seems to work. However I can't access the ServiceResponse's Data inside this value.

((ObjectResult)actual.Result).Value.Data.Id does not work.

I'd appreciate any help, I'm going round in circles.

enter image description here

Here's a pic of my debugger, the Data is in the Value but it can't be accessed as above.

enter image description here

My Controller:

[HttpGet]
    [Route("GetUser/{id}")]
    public async Task<ActionResult<ServiceResponse<GetUserDto>>> GetUser(int id)
    {
        try
        {
            // Validation
            IValidate validate = new Validate();
            int passValidation = validate.numberValidation(id.ToString());

            // 0 is a pass, 1 is a fail.
            if (passValidation == 0)
            {
                return Ok(await _userService.GetUser(id));           // 200 OK
            }

            // Send a message to tell user of the error
            var serviceResponse = new ServiceResponse<GetUserDto>();
            serviceResponse.Data = null;
            serviceResponse.Success = false;
            serviceResponse.Message = "Validation Failed.";

            _logger.LogInformation("Error - Validation Failed in UserController.GetUser - ID: {0} ", id.ToString());

            return Ok(serviceResponse);
        }
        catch(Exception ex)
        {
            _logger.LogError("Error - Exception in UserController.GetUser - {0} ", ex);
            return NotFound();      // 404
        }
    }

And this is my unit test:

        [Fact]
    public async void GetUser_Works()
    {
        using (var mock = AutoMock.GetLoose())
        {
        // Arrange
            int userId = 1;

            var expected = new ServiceResponse<GetUserDto>
            {
               Data = new GetUserDto {
                    Id = userId,
                    UserName = "mockuser1",
                    Email = "[email protected]",
                    FirstName = "barry",
                    LastName = "smith",
                    Dept = "management",
                    Salary = 180000
                },
                Success = true,
                Message = "Success"
            };

            // Mock the Controller call to UserService
            _userService.Setup(x => x.GetUser(userId))
                .ReturnsAsync(expected);


        // Act
            ActionResult<ServiceResponse<GetUserDto>> actual = await _userController.GetUser(userId);


            // Assert
            // Controller returns an OK ActionResult, we need to look inside its "ActionResult.Result.Value" to find the ServiceResponse.
            Assert.Equal(expected.Data.Id, ((ObjectResult)actual.Result).Value);
            Assert.Equal(expected.Data.UserName, ((ObjectResult)actual.Result).Value);
        }
    }

Solution

  • The cast to ObjectResult causes problems, it doesn't fix them. The original problem was caused when return Ok(..) was used.

    To fix this and get a proper ActionResult with a valid value, just return the content without any attempt at casting, ie:

            if (passValidation == 0)
            {
                var user=await _userService.GetUser(id);
                return user;           // 200 OK
            }
    
            // Send a message to tell user of the error
            var serviceResponse = new ServiceResponse<GetUserDto>();
            ...
            return serviceResponse;
    

    You'll be able to access the result through Value without any casting:

            var actual = await _userController.GetUser(userId);
    
            // Assert
    
            var dto=actual.Value;    
    
            Assert.NotNull(dto);        
            Assert.Equal(expected.Data.Id, dto.Data.Id);
            Assert.Equal(expected.Data.UserName, dto.Data.UserName);
    

    The reason for all the confusion is that ActionResult< T > doesn't inherit from ActionResult. As the docs say it's :

    A type that wraps either an TValue instance or an ActionResult.

    The ActionResult<T> class has implicit conversion operators that will return either wrap a Value or an ActionResult like a NotFound and return a new ActionResult<T> object. The two properties can't both have a value at the same time.

    When the controller code used return Ok(serviceResponse) a new OkResult was returned that was wrapped into an ActionResult<T>.

    Proper validation problem response

    Returning 200 for a validation error is a problem in itself. The status code for validation failures is 400 - Bad Request. That's part of the HTTP standard. RFC 7807 specifies how to return problem details for bad requests, including validation errors.

    ASP.NET Core itself will return such a payload if a request fails annotation validation. For programmatic validation in ASP.NET Core MVC, you can add errors to the ModelState and return ValidationProblem() as this article shows:

    [HttpPost]
    public IActionResult Post(PersonResource person)
    {
        if(person.Role == PersonRole.Owner && _people.Any(p => p.Role == PersonRole.Owner))
        {
            ModelState.AddModelError(nameof(PersonResource.Role), "Only one owner is allowed");
            return ValidationProblem();
        }
        _people.Add(person);
    
        return Ok();
    }
    

    This returns :

    HTTP/2 400
    content-type: application/problem+json; charset=utf-8
    date: Sun, 02 Jan 2022 19:58:12 GMT
    server: Kestrel
    
    {
        "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
        "title": "One or more validation errors occurred.",
        "status": 400,
        "traceId": "00-f8108c62afb77a2948020d2390c94956-b7533e5e2c34e08e-00",
        "errors": {
            "Role": [
                "Only one owner is allowed"
            ]
        }
    }
    

    It's also possible to call ValidationProblem with extra information or even a dictionary of error messages :

    var errors=new ModelStateDictionary();
    errors.AddModelError("id","Invalid ID");
    return ValidationError(errors);