如何使用ActionResult进行单元测试<T>?

问题描述 投票:0回答:7

我有一个 xUnit 测试,例如:

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

来源是

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

结果的值为 null,导致我的测试失败,但是如果您查看

ActionResult.Result.Value
(内部属性),它包含预期的解析值。

请参阅以下调试器的屏幕截图。

如何获取要在单元测试中填充的 actionResult.Value?

c# unit-testing asp.net-core .net-core xunit
7个回答
50
投票

在运行时,由于隐式转换,被测试的原始代码仍然可以工作。

但是根据提供的调试器图像,测试似乎断言了结果的错误属性。

因此,虽然更改被测方法可以让测试通过,但无论哪种方式,它在实时运行时都会起作用

ActioResult<TValue>
有两个属性,这两个属性的设置取决于使用它的操作返回的内容。

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

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

来源

因此,当控制器操作使用

Ok()
返回时,它将通过隐式转换设置操作结果的
ActionResult<int>.Result
属性。

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

但是测试断言了

Value
属性(请参阅 OP 中的图像),在本例中未设置该属性。

无需修改被测代码来满足测试,它就可以访问

Result
属性并对该值进行断言

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

37
投票

问题在于

ActionResult<T>
令人困惑的界面,它从来都不是为我们人类使用而设计的。正如其他答案中所述,
ActionResult<T>
具有其
Result
Value
属性集,但不能同时设置两者。当您返回
OkObjectResult
时,框架会填充
Result
属性。当您返回一个对象时,框架会填充
Value
属性。

我为我的测试库创建了以下简单的帮助器,以帮助我在使用

OkObjectResult Ok()
或继承自
ObjectResult

的其他结果时测试返回值
private static T GetObjectResultContent<T>(ActionResult<T> result)
{
    return (T) ((ObjectResult) result.Result).Value;
}

这让我可以执行以下操作:

var actionResult = await controller.Get("foobar");
Assert.IsType<OkObjectResult>(actionResult.Result);
var resultObject = GetObjectResultContent<ObjectType>(actionResult);
Assert.Equal("foobar", resultObject.Id);

20
投票

问题在于将其包装在

Ok
中。如果您返回对象本身,则
Value
会正确填充。

如果您查看微软文档中的示例,他们仅将控制器方法用于非默认响应,例如

NotFound

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

    return product;
}

10
投票

作为 @Nkosi 和 @Otto Teinonen 提供的答案的延续, 有一个 隐式运算符 可以与

Value/Result

相互转换

此外,

ActionResult<TValue>
实现接口
IConvertToActionResult

  public sealed class ActionResult<TValue> : IConvertToActionResult

并提供

Convert
方法,根据空值处理
Result
Value
,请参阅 source 并返回非空值。设置
Result
Value

如果控制器没有返回

Ok(value)
,使用
OkObjectResult
可能会返回null并导致测试失败,例如:

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

下一个扩展方法处理 Result 和 Value 并返回类型化对象 T

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

测试用例

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

5
投票

基于其他答案,我认为这就是你想要的......

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

想要的

.Value
OkObjectResult

里面

我的返回类型是

IEnumerable<MyClass>
所以我使用:

var values = Assert.IsAssignableFrom<IEnumerable<MyClass>>(okResult.Value);

1
投票

另一种方法是序列化,然后以 json 类型进行 desalize:

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

0
投票

NET 6、ControllerBase、ApiController 和 JsonResult 我发现这个方法很有帮助

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

}

然后将

Product
的列表作为返回值

var response = controller.GetList();
var list = ActionResultToList<Product>(response);
© www.soinside.com 2019 - 2024. All rights reserved.