【问题标题】:How to Unit Test with ActionResult<T>?如何使用 ActionResult<T> 进行单元测试?
【发布时间】:2018-12-31 12:21:14
【问题描述】:

我有一个 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


    【解决方案1】:

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

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

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

    ActioResult&lt;TValue&gt; 有两个属性,这些属性的设置取决于使用它的操作返回的内容。

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

    Source

    因此,当控制器操作使用Ok() 返回时,它将通过隐式转换设置操作结果的ActionResult&lt;int&gt;.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();
    }
    

    【讨论】:

    • 好的。因此,似乎要么填充了ActionResult.Result,要么填充了ActionResult.Value,但不是同时填充了两者。我没有从文档中发现这一点。 docs.microsoft.com/en-us/dotnet/api/…
    • @RichardCollette 我怀疑他们会过多提及它,因为该对象实际上是由框架处理的。
    【解决方案2】:

    问题在于将其包装在 Ok 中。如果您返回对象本身,Value 将正确填充。

    如果您查看Microsoft's examples in the docs,它们只使用控制器方法来处理非默认响应,例如NotFound

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

    【讨论】:

    • 我打算对这个答案提出异议,因为我认为它不适用于异步操作。事实证明它工作正常,我的问题是我返回了一个 Task&lt;ActionResult&lt;IEnumerable&lt;Product&gt;&gt;&gt; 并且我必须先重新确认(即.ToList())该集合。为你 +1!
    【解决方案3】:

    问题在于ActionResult&lt;T&gt; 的令人困惑的界面,它从未被设计为供我们人类使用。如其他答案所述,ActionResult&lt;T&gt; 具有其 ResultValue 属性集,但不是两者兼有。当您返回 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);
    

    【讨论】:

      【解决方案4】:

      作为@Nkosi 和@Otto Teinonen 提供的答案的延续, 有一个implicit opertator 可以转换为Value/Result/从Value/Result

      另外,ActionResult&lt;TValue&gt; 实现接口IConvertToActionResult

        public sealed class ActionResult<TValue> : IConvertToActionResult
      

      并提供Convert 方法,根据它们的空值处理ResultValue,请参阅source 并返回非空值。 ResultValue 已设置。

      如果 Controler 没有返回 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】:

        另一种方法是在json类型中序列化然后去盐:

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

        【讨论】:

          【解决方案6】:

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

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

          所需的.ValueOkObjectResult

          我的返回类型是IEnumerable&lt;MyClass&gt;,所以我使用了:

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

          【讨论】:

            猜你喜欢
            • 2019-04-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-01-08
            • 2021-03-15
            相关资源
            最近更新 更多