使用 Xunit 进行 .Net 核心测试

.Net core testing with Xunit

这是我第一次编写测试用例,我并没有被卡住,也不确定如何继续。

我有以下API。在下面的示例中,我有 2 个要执行测试的端点。

public class ValuesController : Controller
{
    //This interface is used to setup dynamo db and connection to aws
    private IDynamoDbClientInitialization _clientAccessor;
    private static string dynamoDbTable = string.Empty; 

    public ValuesController(IOptions<Dictionary<string, string>> appSettings, IDynamoDbClientInitialization clientAccessor)
    {                      
         var vals = appSettings.Value;            
         dynamoDbTable = vals["dynamoDbTable"];
        _clientAccessor = clientAccessor;
    }

    [HttpGet("api/data")]
    public async Task<List<MyModel>> GetAllData(string type, string status)
    {
        List<ScanCondition> conditions = new List<ScanCondition>();
        conditions.Add(new ScanCondition("Type", ScanOperator.Equal, type));
        conditions.Add(new ScanCondition("Status", ScanOperator.Equal, status));
        var response = await _clientAccessor.GetContext().ScanAsync<MyModel>(conditions, AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync();
        return results.Select(x => x.UpdatedBy.ToLower()).ToList();
    }

     [HttpPost("api/save")]
    public async Task<IActionResult> SaveData([FromBody] List<MyModel> listData, string input, string name, string type)
    {
       List<MyModel> model = null; 
       foreach (var data in listData)
       {
         //populating data here
          await _clientAccessor.GetContext().SaveAsync(data, AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable));  
       }         

       return Ok();
    }
}       

public class DynamoDbClientInitialization : IDynamoDbClientInitialization
{
    private readonly DynamoDbClientSettings settings;
    private DynamoDBContext _awsContext;

    public DynamoDbClientInitialization(IOptions<DynamoDbClientSettings> options)
    {
        settings = options?.Value;
    }

    public DynamoDBContext GetContext()
    {         
        //Check is context already exists. If not create a new one.
        if(_awsContext != null)
        {
            return _awsContext;
        }
        else
        {                
            var creds = AWSHelperMethods.SetAwsCredentials(settings.Id, settings.Password);
            var dynamoClient = AWSHelperMethods.GetDynamoDbClient(creds, settings.Region);
            _awsContext = AWSHelperMethods.GetDynamoDbContext(dynamoClient);

            return _awsContext;
        }           
    }
}

public static class AWSHelperMethods
{
   public static BasicAWSCredentials SetAwsCredentials(string awsId, string awsPassword)
    {
        var creds = new BasicAWSCredentials(awsId, awsPassword);
        return creds;
    }

    public static AmazonDynamoDBClient GetDynamoDbClient(BasicAWSCredentials creds, RegionEndpoint awsDynamoDbRegion)
    {
        var client = new AmazonDynamoDBClient(creds, awsDynamoDbRegion);
        return client;
    }

    public static DynamoDBContext GetDynamoDbContext(AmazonDynamoDBClient client)
    {
        var context = new DynamoDBContext(client);
        return context;
    }

    public static DynamoDBOperationConfig GetDynamoDbOperationConfig(string dynamoDbTable)
    {
        DynamoDBOperationConfig config = new DynamoDBOperationConfig() { OverrideTableName = dynamoDbTable };
        return config;
    }   
 }

下面是我添加的xunit工程。在这里,我使用最小起订量来最小化我的 aws 连接和其他连接。问题在下面针对代码的评论中。

public class DataTest
{

    [Fact]
    public void PassingTest()
    {
       //Arrange
       var dynamoDbTable = "someValue";

       //Trying to moq IOptions
       var moqOp = new Mock<IOptions<Dictionary<string, string>>>();

       //Create an instance to hold desired values
       var vals = new Dictionary<string, string>();

       //Set expected value
       vals["dynamoDbTable"] = dynamoDbTable;

       //Setup dependency behavior
       moqOp.Setup(_ => _.Value).Returns(vals);

       //Trying to moq my connection  
       var moqDb = new Mock<IDynamoDbClientInitialization>();

       //Fake data
       List<MyModel> data = new List<MyModel>()
      { 
        //populate as needed
       };

     moqDb
     .Setup(_ => _.GetContext().ScanAsync<MyModel>
     (It.IsAny<List<ScanCondition>>(), AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync())
.ReturnsAsync(data);

      ValuesController controller = new ValuesController(moqOp.Object, 
       moqDb.Object);

      var actual =  controller.GetAllData();
    }       
}

以上我得到的错误是: 表达式树不能包含使用可选参数的调用或调用

这是在线

.Setup(_ => _.GetContext().ScanAsync<MyModel>
 (It.IsAny<List<ScanCondition>>(), AWSHelperMethods.GetDynamoDbOperationConfig(dynamoDbTable)).GetRemainingAsync())

谁能帮忙解决一下?

---已更新---

    public interface IDynamoDbManager
    {
        Task<List<T>> GetAsync(IEnumerable<ScanCondition> conditions);

        Task SaveAsync(T item);
    }

An expression tree may not contain a call or invocation that uses optional arguments

您正在尝试 "call" 尚未设置 GetContext() 的方法 ScanAsync。要解决此问题,您必须在尝试设置 ScanAsync()

之前设置 GetContext() 的 return 值

这段代码很难测试,所以让我们重构它。

您必须将 _clientAccessor.GetContext() 的直接调用更改为注入 IDynamoDBContext。 DynamoDbClientInitialization 没有意义,因为可以轻松替换为 IAmazonDynamoDb。要摆脱冗长的配置阅读代码行,请使用

services.AddAWSService<IAmazonDynamoDB>();

所有对 DynamoDb 的调用都应封装在单独的 class 调用中,例如 DynamoDbManager

public class DynamoDbManager<T> : DynamoDBContext, IDynamoDbManager<T> where T : class
    {
        private DynamoDBOperationConfig _config;

        public DynamoDbManager(IAmazonDynamoDB client, string tableName): base(client)
        {
            _config = new DynamoDBOperationConfig()
            {
                OverrideTableName = tableName
            };
        }

        public Task<List<T>> GetAsync(IEnumerable<ScanCondition> conditions)
        {
            return ScanAsync<T>(conditions, _config).GetRemainingAsync();
        }

        public Task SaveAsync(T item)
        {
            return base.SaveAsync(item, _config);
        }
    }

现在你的控制器将变成这样 ValuesController

public class ValuesController : Controller
    {
        private readonly IDynamoDbManager<MyModel> _dynamoDbManager;
        //This interface is used to setup dynamo db and connection to aws
        private static string dynamoDbTable = string.Empty;

        public ValuesController(IOptions<Dictionary<string, string>> appSettings, IDynamoDbManager<MyModel> dynamoDbManager)
        {
            _dynamoDbManager = dynamoDbManager;
            var vals = appSettings.Value;
            dynamoDbTable = vals["dynamoDbTable"];
        }

        [HttpGet("api/data")]
        public async Task<IActionResult> GetAllData(string type, string status)
        {
            var conditions = new List<ScanCondition>
            {
                new ScanCondition("Type", ScanOperator.Equal, type),
                new ScanCondition("Status", ScanOperator.Equal, status)
            };
            var result = await _dynamoDbManager.GetAsync(conditions);
            var response = result.Select(_ => _.UpdatedBy.ToLower()).ToList();
            return Ok(response);
        }

        [HttpPost("api/save")]
        public async Task<IActionResult> SaveData([FromBody] List<MyModel> listData, string input, string name, string type)
        {
            foreach (var data in listData)
            {
                //populating data here
                await _dynamoDbManager.SaveAsync(data);
            }
            return Ok();
        }
    }

重构完成,开始编写单元测试 ValuesControllerTests

public class ValuesControllerTests
    {
        private Mock<IDynamoDbManager<MyModel>> _dbManager;
        private ValuesController _valuesController;

        public ValuesControllerTests()
        {
            var mockRepository = new MockRepository(MockBehavior.Loose);
            _dbManager = mockRepository.Create<IDynamoDbManager<MyModel>>();
            var options = new OptionsWrapper<Dictionary<string, string>>(new Dictionary<string, string>()
            {
                {"dynamoDbTable", nameof(MyModel) }
            });
            _valuesController = new ValuesController(options, _dbManager.Object);

        }

        [Fact]
        public async Task GetAllData_ShouldSelectUpdateByInLowerCase()
        {
            //
            var searchResult = new List<MyModel>()
            {
                new MyModel() {UpdatedBy = "UpdatedBy1"}
            };
            _dbManager
                .Setup(_ => _.GetAsync(It.Is<List<ScanCondition>>(list => list.Count == 2)))
                .ReturnsAsync(searchResult);

            //

            var result = await _valuesController.GetAllData("typeValue", "statusValue");

            //
            var okResult = result as OkObjectResult;
            Assert.NotNull(okResult);
            var values = okResult.Value as List<string>;
            Assert.Contains("updatedby1", values);
        }

        [Fact]
        public async Task SaveData_ShouldCallSaveForAllRequestedData()
        {
            //
            var listData = new List<MyModel>()
            {
                new MyModel(),
                new MyModel(),
                new MyModel()
            };
            _dbManager
                .Setup(_ => _.SaveAsync(It.IsAny<MyModel>()))
                .Returns(Task.CompletedTask);

            //

            var result = await _valuesController.SaveData(listData, "","", "");

            //
            _dbManager.Verify(_ => _.SaveAsync(It.IsAny<MyModel>()), Times.Exactly(3));
        }
    }

就是这样,我为每个控制器操作做了两次测试,因为它更容易理解 - 如果你真的只需要一个测试,你可以轻松加入它们