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

Ric*_*tte 8 c# unit-testing xunit .net-core asp.net-core

我有一个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();
}
Run Code Online (Sandbox Code Playgroud)

来源是

/// <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()));
}
Run Code Online (Sandbox Code Playgroud)

结果的值为null,导致我的测试失败,但是如果您查看ActionResult.Result.Value(内部属性),则该结果包含预期的解析值。

请参见以下调试器的屏幕截图。 在此处输入图片说明

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

Eri*_*sen 14

问题是将其包装在Ok. 如果返回对象本身,Value则填充正确。

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

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

    return product;
}
Run Code Online (Sandbox Code Playgroud)

  • 我会对这个答案提出异议,因为我认为它不适用于异步操作。事实证明它工作正常,我的问题是我返回了一个 `Task&lt;ActionResult&lt;IEnumerable&lt;Product&gt;&gt;&gt;` 并且我必须首先具体化(即 `.ToList()`)集合。为你+1! (2认同)

Ott*_*nen 12

问题在于其令人困惑的界面ActionResult<T>从未被设计为供我们人类使用。正如其他答案中所述,设置了ActionResult<T>它的ResultValue属性,但不是同时设置了两者。当您返回时OkObjectResult,框架会填充该Result属性。当您返回一个对象时,框架会填充该Value属性。

我为我的测试库创建了以下简单的帮助程序,以帮助我测试使用时的返回值OkObjectResult Ok()或其他继承自的结果ObjectResult

private static T GetObjectResultContent<T>(ActionResult<T> result)
{
    return (T) ((ObjectResult) result.Result).Value;
}
Run Code Online (Sandbox Code Playgroud)

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

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


M.H*_*san 12

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

另外,ActionResult<TValue>实现接口IConvertToActionResult

  public sealed class ActionResult<TValue> : IConvertToActionResult
Run Code Online (Sandbox Code Playgroud)

并提供Convert处理两者的方法Result,并Value根据它们的空值,查看源代码并返回非空值。或ResultValue设置。

如果 Controler 没有返回Ok(value),则 usingOkObjectResult可能会返回 null 并导致测试失败,例如:

 var result = actionResult.Result as OkObjectResult; //may be null
Run Code Online (Sandbox Code Playgroud)

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

  public sealed class ActionResult<TValue> : IConvertToActionResult
Run Code Online (Sandbox Code Playgroud)

测试用例

[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);
 }
Run Code Online (Sandbox Code Playgroud)


Nko*_*osi 7

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

但是根据提供的调试器映像,看起来测试似乎在对结果的错误属性进行断言。

因此,尽管更改被测方法允许测试通过,但以任何一种方式实时运行时都可以工作

ActioResult<TValue> 根据使用它的操作返回的内容设置了两个属性。

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

/// <summary>
/// Gets the value.
/// </summary>
public TValue Value { get; }
Run Code Online (Sandbox Code Playgroud)

资源

因此,使用返回的控制器动作时Ok(),将ActionResult<int>.Result通过隐式转换设置动作结果的属性。

public static implicit operator ActionResult<TValue>(ActionResult result)
{
    return new ActionResult<TValue>(result);
}
Run Code Online (Sandbox Code Playgroud)

但是测试正在声明该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();
}
Run Code Online (Sandbox Code Playgroud)

  • 好。因此看来,要么填充了“ ActionResult.Result”,要么填充了“ ActionResult.Value”,但不是两者都填充。我从文档中没有发现这一点。https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.actionresult-1?view=aspnetcore-2.1 (2认同)

Pet*_*r L 5

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

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

所需的内容.Value在里面OkObjectResult

我的返回类型是IEnumerable<MyClass>这样的:

var values = Assert.IsAssignableFrom<IEnumerable<MyClass>>(okResult.Value);
Run Code Online (Sandbox Code Playgroud)