我有一个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以便在单元测试中填充?
答案 0 :(得分:5)
问题出在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);
答案 1 :(得分:3)
在运行时,由于隐式转换,您正在测试的原始代码仍然可以工作。
但是根据所提供的调试器映像,看起来测试似乎在对结果的错误属性进行断言。
因此,在更改被测方法以允许测试通过的情况下,当以任何一种方式实时运行时都可以正常工作
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();
}
答案 2 :(得分:3)
作为@Nkosi 和@Otto Teinonen 提供的答案的延续,
有一个 implicit opertator 要转换为/从 Value/Result
另外,ActionResult<TValue>
实现接口IConvertToActionResult
public sealed class ActionResult<TValue> : IConvertToActionResult
并提供 Convert
方法,根据它们的空值处理 Result
和 Value
,请参阅 source 并返回非空值。设置了 Result
或 Value
。
如果 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);
}
答案 3 :(得分:1)
另一种方法是在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" });
}
答案 4 :(得分:0)
问题是将其包装在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;
}