How to Unit Test with ActionResult<T>?
Asked Answered
P

8

70

I have a xUnit test like:

[Fact]
public async void GetLocationsCountAsync_WhenCalled_ReturnsLocationsCount()
{
    _locationsService.Setup(s => s.GetLocationsCountAsync("123")).ReturnsAsync(10);
    var controller = new LocationsController(_locationsService.Object, null)
    {
        ControllerContext = { HttpContext = SetupHttpContext().Object }
    };
    var actionResult = await controller.GetLocationsCountAsync();
    actionResult.Value.Should().Be(10);
    VerifyAll();
}

Source is

/// <summary>
/// Get the current number of locations for a user.
/// </summary>
/// <returns>A <see cref="int"></see>.</returns>
/// <response code="200">The current number of locations.</response>
[HttpGet]
[Route("count")]
public async Task<ActionResult<int>> GetLocationsCountAsync()
{
    return Ok(await _locations.GetLocationsCountAsync(User.APropertyOfTheUser()));
}

The value of the result is null, causing my test to fail, but if you look at ActionResult.Result.Value (an internal property) it contains the expected resolved value.

See the following screen capture of the debugger. enter image description here

How do I get the actionResult.Value to populate in a unit test?

Pinto answered 24/7, 2018 at 1:10 Comment(0)
S
56

At run time your original code under test would still work because of the implicit conversion.

But based on the provided debugger image it looks like the test was asserting on the wrong property of the result.

So while changing the method under test allowed the test to pass, it would have worked when run live either way

ActioResult<TValue> has two properties that are set depending on what is returned from the action that uses it.

/// <summary>
/// Gets the <see cref="ActionResult"/>.
/// </summary>
public ActionResult Result { get; }

/// <summary>
/// Gets the value.
/// </summary>
public TValue Value { get; }

Source

So when the controller action returned using Ok() it would set the ActionResult<int>.Result property of the action result via implicit conversion.

public static implicit operator ActionResult<TValue>(ActionResult result)
{
    return new ActionResult<TValue>(result);
}

But the test was asserting the Value property (refer to image in OP), which in this case was not being set.

Without having to modify the code under test to satisfy the test it could have accessed the Result property and make assertions on that value

[Fact]
public async Task GetLocationsCountAsync_WhenCalled_ReturnsLocationsCount() {
    //Arrange
    _locationsService
        .Setup(_ => _.GetLocationsCountAsync(It.IsAny<string>()))
        .ReturnsAsync(10);
    var controller = new LocationsController(_locationsService.Object, null) {
        ControllerContext = { HttpContext = SetupHttpContext().Object }
    };

    //Act
    var actionResult = await controller.GetLocationsCountAsync();

    //Assert
    var result = actionResult.Result as OkObjectResult;
    result.Should().NotBeNull();
    result.Value.Should().Be(10);

    VerifyAll();
}
Selfabuse answered 24/7, 2018 at 2:13 Comment(2)
Ok. So it appears that either ActionResult.Result is populated or ActionResult.Value is populated but not both. I didn't find that evident from the documentation. learn.microsoft.com/en-us/dotnet/api/…Pinto
@RichardCollette I doubt they would have made much mention of it as the object was really meant to be dealt with by the framework.Selfabuse
U
43

The problem lies in the confusing interface of ActionResult<T> that was never designed to be used by us humans. As stated in other answers, ActionResult<T> has either its Result or Value property set but not both. When you return an OkObjectResult the framework populates the Result property. When you return an object the framework populates the Value property.

I created the following simple helper for my test library to help me test the return values when I am using OkObjectResult Ok() or other results inheriting from ObjectResult

private static T GetObjectResultContent<T>(ActionResult<T> result)
{
    return (T) ((ObjectResult) result.Result).Value;
}

This lets me do the following:

var actionResult = await controller.Get("foobar");
Assert.IsType<OkObjectResult>(actionResult.Result);
var resultObject = GetObjectResultContent<ObjectType>(actionResult);
Assert.Equal("foobar", resultObject.Id);
Ube answered 2/8, 2020 at 15:11 Comment(3)
Nice. Can also be an extension method and used like this: actionResult.GetObjectResultContent()Polygnotus
Very helpful. Simple explanation and a great piece of helper code.Doherty
thanks so much for this. really nice and clean solutionSmashed
C
22

The issue is wrapping it in Ok. If you return the object itself, Value is populated correctly.

If you look at Microsoft's examples in the docs, they only use the controller methods for non-default responses like NotFound:

[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Product> GetById(int id)
{
    if (!_repository.TryGetProduct(id, out var product))
    {
        return NotFound();
    }

    return product;
}
C answered 23/6, 2019 at 15:55 Comment(1)
I was going to take issue with this answer since I didn't think it worked for async actions. Turns out that it works fine and my problem was that I returned a Task<ActionResult<IEnumerable<Product>>> and I had to reify (i.e. .ToList()) the collection first. +1 for you!Plosive
K
12

As a continuation to the answer provided by @Nkosi and @Otto Teinonen, there is an implicit opertator to convert to/from Value/Result

Also, ActionResult<TValue> implement the interface IConvertToActionResult

  public sealed class ActionResult<TValue> : IConvertToActionResult

and provide Convert method that handle both Result and Value based on their null values, see the source and return the non null value. Either Result or Value is set.

If the Controler didn't return Ok(value), using OkObjectResult may return null and cause test fail, like:

 var result = actionResult.Result as OkObjectResult; //may be null

The next extension method handle both Result and Value and return the typed object T

public static T GetObjectResult<T>(this ActionResult<T> result)
        {
            if (result.Result != null)
                return (T)((ObjectResult)result.Result).Value;
            return result.Value;            
        }

Test case

[Test]
public async Task Test()
{
    var controller = new ProductsController(Repo);
    var result = await controller.GetProduct(1);
    await Repo.Received().GetProduct(1);
     //result.Result.Should().BeOfType<OkObjectResult>();  // may fail if controller didn't return Ok(value)
     result.GetObjectResult().ProductId.Should().Equals(1);
 }
Katherinakatherine answered 4/1, 2021 at 6:35 Comment(0)
K
6

Building on other answers, I think this is what you want...

var actionResult = await controller.GetLocationsCountAsync();
var okResult = Assert.IsType<OkObjectResult>(actionResult.Result);
var value = Assert.IsType<int>(okResult.Value);
Assert.Equal(10, value);

The desired .Value is inside the OkObjectResult

My return type was IEnumerable<MyClass> so I used:

var values = Assert.IsAssignableFrom<IEnumerable<MyClass>>(okResult.Value);
Ketron answered 12/12, 2021 at 3:2 Comment(0)
I
1

Another method would be to serialize and then desalize in json type:

[Fact]
public void Test1()
{
    var controller = new ProductController();
    var result = controller.Products();
    var okResult = Assert.IsType<OkObjectResult>(result);
    if (okResult.Value == null) return;
    var json = JsonConvert.SerializeObject(okResult.Value);
    var values = JsonConvert.DeserializeObject<Product>(json);
    if (values != null) Assert.Equal(1, values.Id);
}

private class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

[Route("api/product")]
[ApiController]
public class ProductController : ControllerBase
{
    [HttpGet]
    public IActionResult Products() => Ok(new { Id = 1, Name = "Smartphone" });
}
Interosculate answered 17/5, 2021 at 15:26 Comment(0)
E
0

NET 6, ControllerBase, ApiController and JsonResult<List> I found this method helpful

public List<T> ActionResultToList<T>(ActionResult<IEnumerable<T>> response)
{
   return ((Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<T>)
          ((Microsoft.AspNetCore.Mvc.JsonResult)response.Result).Value)
          .ToList();

}

Then for List of Product as return value

var response = controller.GetList();
var list = ActionResultToList<Product>(response);
Ellersick answered 29/8, 2023 at 18:51 Comment(0)
S
0

This extension method might be useful:

public static class ActionResultExtensions
{
public static T ReturnValue<T>(this ActionResult<T> result)
    {
    return result.Result == null ? result.Value : (T)((ObjectResult)result.Result).Value;
    }
}

then you can use it like this:

Assert.IsTrue(response.ReturnValue<bool>());
Spleenful answered 18/10, 2023 at 19:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.