.Net core testing with Xunit

2020-04-16 03:23发布

This is the first time I am writing test case and I am not sort of stuck and not sure how to proceed further.

I have the following API. In the below sample I have 2 endpoints which I want to perform testing.

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;
    }   
 }

Below is the xunit project that I added. Here I am using MOQ to moq up my aws connection and others. Questions are below in comments against the code.

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();
    }       
}

Above I am getting the error as: An expression tree may not contain a call or invocation that uses optional arguments

This is on line

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

Can anyone help to resolve?

---updated---

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

        Task SaveAsync(T item);
    }

1条回答
可以哭但决不认输i
2楼-- · 2020-04-16 04:26

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

You are trying to "call" a method ScanAsync on not already set up GetContext(). To solve this you have to Setup return value for GetContext() before you tried to Setup ScanAsync()


This code is very hard to test so let's refactor it.

You have to change direct calls of _clientAccessor.GetContext() to injection of IDynamoDBContext. DynamoDbClientInitialization doesn't make sense as can be easily replaced with IAmazonDynamoDb. To get rid of verbose config reading code lines, use

services.AddAWSService<IAmazonDynamoDB>();

All calls to DynamoDb should be encapsulated on a separated class called, for example, 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);
        }
    }

Now your controller will be changed in this way 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();
        }
    }

Refactoring is finished, start writing unit tests 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));
        }
    }

That's all, I did two tests for each controller action because its far more understandable - if you realy need only one test you can easily join them

查看更多
登录 后发表回答