【问题标题】:How to create a unit test for an IActionResult controller action which returns decimal?如何为返回十进制的 IActionResult 控制器操作创建单元测试?
【发布时间】:2021-12-16 13:22:22
【问题描述】:

我是单元测试的新手,也很困惑,希望你能帮助我理解一些事情, 我想为以下场景编写一个 Xunit 测试:

我有以下控制器:

    [ApiController]
    [Route("api/[controller]")]
     public class HomeController : ControllerBase
    {
        public readonly IDataService _dataService;
        public HomeController(IDataService dataservice)
        {

            _dataService = dataservice;
        }
        [HttpGet("/value")]
        public IActionResult GetTotal(string name, string year)
        {

            var totalAmount = _dataService.GetToTalValue(name, year);
            return Ok(totalAmount.Result);
        }

我有存储库类来与数据库对话,我将其命名为 dataservice:

 public  interface IDataService
{
  
    public Task<decimal> GetTotal(string name, string year)
}

实现很简单,我只写我返回的内容以制作长篇故事:

        public Task<decimal> GetTotal(string name, string year){
    ///here i connect to db and get data and return to controller 

     return Task.FromResult(totalAmount);
    }

最后,我按照一些在线教程创建了一个单元测试:

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
       var sut = new Mock<IDataService>();

        sut.Setup(s => s.GetTotal("ASK", "1235")).ReturnsAsync(1231);

        HomeController mm = new HomeController(sut.Object);

        var result = mm.GetTotal("ASK", "1235");

       ///asset gives error because cant convert iactionresult to decimal
    }
}

}

首先我不明白为什么我们需要写:

  sut.Setup(s => s.GetTotal("ASK", "1235")).ReturnsAsync(1231);

因为我们将再次使用控制器进行确切的操作:

 HomeController mm = new HomeController(sut.Object);
 var result = mm.GetTotal("ASK", "1235");

该设置意味着我将两个值传递给 GetTotal 并期望 1231 对吗?那么测试控制器的意义何在?请帮助我理解它,我的第二个问题是我从 iactionresult 得到的结果是十进制的,但在断言中我得到错误,我无法将 iactionresult 转换为十进制

我们将不胜感激任何帮助

【问题讨论】:

    标签: c# unit-testing asp.net-core


    【解决方案1】:

    鉴于服务被定义为异步,该操作也应该是异步的

        [HttpGet("/value")]
        public async Task<IActionResult> GetTotal(string name, string year) {
            decimal totalAmount = await _dataService.GetToTalValue(name, year);
            return Ok(totalAmount);
        }
    

    否则保持一切同步

    对于单元测试,您正在孤立地验证被测对象的预期行为。

    由于主题是异步的,那么测试也应该是异步的

    public class UnitTest1 {
        [Fact]
        public async Task Test1() {
            //Arrange
            string name = "ASK";
            string year = "1235";
            decimal expected = 1231M;
    
            //setup expectations of the mocked dependency
            var mock = new Mock<IDataService>();    
            mock.Setup(_ => _.GetTotalValue(name, year)).ReturnsAsync(expected);
    
            HomeController sut = new HomeController(mock.Object);
    
            //Act
            IActionResult result = await sut.GetTotal(name, year);
            //Need to convert to actual action result Type
            OkObjectResult okResult = result as OkObjectResult;
    
            //Assert
            okResult.Should().NotBeNull(); //using FluentAssertions
            //need to extract the actual value within the result
            decimal actual = (decimal)okResult.Value;
            //verify expected behavior
            actual.Should().Be(expected); //using FluentAssertions
        }
    }
    

    由于给定示例的简单性质,这​​已被简化,但这里的最终目标是通过给它已知值并断言预期结果来验证被测对象的预期行为。

    由于在这种情况下被测试的对象是控制器,因此实际的IDataService 接口的实现对于这个隔离测试的目的来说是无关紧要的。模拟所需的行为并将模拟的依赖项提供给控制器(主题)。

    【讨论】:

    • 感谢有关设置部分的一个问题,实际上 GetTotalValue 从我的数据库中获取数据,当我在这里说我期望(预期)时,意味着当我将特定年份和名称传递给我的数据库时使用应该得到预期?老实说,我的问题在于设置部分
    • @mortezasol 对。在实际运行时应用程序中,值将来自数据库(或其他任何地方)。对于测试,您想要控制/伪造值的来源。在这种情况下,它将来自配置的模拟。
    【解决方案2】:

    如果你模拟一个接口,你没有测试接口中方法的结果,你认为数据服务正在工作并强制结果。这应该是

    sut.Setup(s => s.GetToTalValue("ASK", "1235")).ReturnsAsync(10);
    

    你必须围绕这个接口测试其他逻辑。 例如,如果控制器正确传递参数并返回正确的值

    [Fact]
    public void Test1()
    {
        decimal resultTotal = 10;
        string name = "ASK";
        string year = "1235";
    
        var mockDataService = new Mock<IDataService>();
            
        mockDataService.Setup(s => s.GetToTalValue(name, year)).ReturnsAsync(resultTotal);
    
        HomeController sut = new HomeController(mockDataService.Object);
    
        IActionResult result = sut.GetTotal(name, year);
    
        Assert.True(result is OkObjectResult);
        Assert.True((decimal)((result as OkObjectResult)?.Value) == resultTotal);
    }
    

    【讨论】:

      【解决方案3】:

      第一个问题非常固执,这是一个非常简单的Action,我只是为了“测试”和一些回归测试而编写测试,如果添加更多逻辑,我肯定会为更改编写测试.

      第二个问题,OK()OkObjectResult 的便捷方法,所以可以这样转换:

      var result = (OkObjectResult) mm.GetTotal("ASK", "1235");
      
      Assert.True(result.Value == 1231);
      

      另外,你在嘲笑IDataService 并称它为sut,sut 将是控制器,SUT 通常不会被嘲笑或存根,这又是自以为是...:-)

      【讨论】:

      • 谢谢,但我仍然不明白这里的模拟点,我将一些值传递给我的控制器来测试它,我不能在这里注入我的 IDataService 而不是使用 var sut = new Mock( );
      • 您正在测试操作的逻辑,在您的情况下几乎没有。模拟(在您的情况下为存根)IDataService 有助于促进依赖期望,仅此而已。更复杂的 Action 方法可以说明这一点。但是如上所述,出于回归原因和代码覆盖率,我仍然会保留您的测试。选择更复杂的Action方法,会更有意义。
      猜你喜欢
      • 2017-05-08
      • 1970-01-01
      • 1970-01-01
      • 2017-03-07
      • 2020-09-05
      • 2016-03-27
      • 1970-01-01
      • 2022-07-27
      • 1970-01-01
      相关资源
      最近更新 更多